Skip to content

Commit 1ac021e

Browse files
committed
feature: ffmpeg cuda option
1 parent 13670ff commit 1ac021e

File tree

14 files changed

+589
-277
lines changed

14 files changed

+589
-277
lines changed

cmd/tinytune/tinytune.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ func main() {
9595
Destination: &rawConfig.Video,
9696
Category: ProcessingCLICategory,
9797
},
98+
&cli.StringFlag{
99+
Name: "video-processing-accel",
100+
Value: string(preview.Auto),
101+
Usage: "processing type for videos: 'auto', 'hardware', 'software'",
102+
Destination: &rawConfig.VideoProcessingAccel,
103+
Category: ProcessingCLICategory,
104+
},
98105
&cli.BoolFlag{
99106
Name: "image",
100107
Value: rawConfig.Images,
@@ -240,6 +247,7 @@ func start(config internal.Config) {
240247
preview.WithMaxVideos(config.Process.Video.MaxItems),
241248
preview.WithMaxFileSize(config.Process.MaxFileSize),
242249
preview.WithTimeout(config.Process.Timeout),
250+
preview.WithVideoAccel(config.Process.VideoAccel),
243251
)
244252
internal.PanicError(err)
245253

internal/cli.go

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,27 @@ import (
1010
"time"
1111

1212
"github.com/alxarno/tinytune/pkg/bytesutil"
13+
"github.com/alxarno/tinytune/pkg/preview"
1314
)
1415

1516
const defaultPort = 8080
1617
const maxPortNumber = 65536
1718

1819
type RawConfig struct {
19-
Dir string
20-
Parallel int
21-
Video bool
22-
Images bool
23-
MaxImages int64
24-
MaxVideos int64
25-
Includes string
26-
Excludes string
27-
MaxFileSize string
28-
Streaming string
29-
MediaTimeout string
30-
IndexFileSave bool
31-
Port int
20+
Dir string
21+
Parallel int
22+
Video bool
23+
VideoProcessingAccel string
24+
Images bool
25+
MaxImages int64
26+
MaxVideos int64
27+
Includes string
28+
Excludes string
29+
MaxFileSize string
30+
Streaming string
31+
MediaTimeout string
32+
IndexFileSave bool
33+
Port int
3234
}
3335

3436
type MediaTypeConfig struct {
@@ -54,6 +56,7 @@ func (c MediaTypeConfig) Print(name string) {
5456
type ProcessConfig struct {
5557
Parallel int
5658
Video MediaTypeConfig
59+
VideoAccel preview.VideoProcessingAccelType
5760
Timeout time.Duration
5861
Image MediaTypeConfig
5962
Includes []*regexp.Regexp
@@ -90,7 +93,11 @@ func (c ProcessConfig) Print() {
9093
}
9194

9295
if c.MaxFileSize != -1 {
93-
slog.String("max-file-size", bytesutil.PrettyByteSize(c.MaxFileSize))
96+
params = append(params, slog.String("max-file-size", bytesutil.PrettyByteSize(c.MaxFileSize)))
97+
}
98+
99+
if c.VideoAccel != preview.Auto {
100+
params = append(params, slog.String("video-processing-accel", string(c.VideoAccel)))
94101
}
95102

96103
slog.Info(
@@ -128,8 +135,10 @@ func (c Config) Print() {
128135

129136
func DefaultRawConfig() RawConfig {
130137
return RawConfig{
131-
Dir: os.Getenv("PWD"),
132-
Parallel: runtime.NumCPU(),
138+
Dir: os.Getenv("PWD"),
139+
Parallel: runtime.NumCPU(),
140+
VideoProcessingAccel: string(preview.Auto),
141+
// Parallel: 8,
133142
Port: defaultPort,
134143
Video: true,
135144
Images: true,
@@ -156,6 +165,7 @@ func NewConfig(raw RawConfig) Config {
156165
Timeout: getDuration(raw.MediaTimeout),
157166
Parallel: raw.Parallel,
158167
Video: MediaTypeConfig{raw.Video, raw.MaxVideos},
168+
VideoAccel: preview.VideoProcessingAccelType(raw.VideoProcessingAccel),
159169
Image: MediaTypeConfig{raw.Images, raw.MaxImages},
160170
Includes: getRegularExpressions(raw.Includes),
161171
Excludes: getRegularExpressions(raw.Excludes),

pkg/index/builder.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ var (
2020
)
2121

2222
type PreviewGenerator interface {
23-
Pull(item preview.Source) (preview.Data, error)
23+
Pull(ctx context.Context, item preview.Source) (preview.Data, error)
2424
Close()
2525
}
2626

@@ -131,7 +131,7 @@ func (ib *indexBuilder) loadFile(
131131
defer sem.Release(1)
132132
defer wg.Done()
133133

134-
preview, err := ib.params.preview.Pull(metaItem)
134+
preview, err := ib.params.preview.Pull(ctx, metaItem)
135135
if err != nil {
136136
slog.Error(fmt.Errorf("%w (%s): %w", ErrPreviewPull, metaItem.RelativePath, err).Error())
137137
dst <- loadedFile{metaItem, nil}

pkg/index/index_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ func (m mockPreviewData) Duration() time.Duration {
122122
}
123123

124124
//nolint:ireturn
125-
func (mock mockPreviewGenerator) Pull(_ preview.Source) (preview.Data, error) {
125+
func (mock mockPreviewGenerator) Pull(_ context.Context, _ preview.Source) (preview.Data, error) {
126126
return mockPreviewData{data: mock.sampleData}, nil
127127
}
128128

pkg/preview/ffmpeg.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package preview
2+
3+
import (
4+
"fmt"
5+
"slices"
6+
"time"
7+
8+
"github.com/alxarno/tinytune/pkg/timeutil"
9+
)
10+
11+
type ffmpegHWAccelType string
12+
13+
const (
14+
ffmpegSoftwareAccel ffmpegHWAccelType = "software"
15+
ffmpegCudaAccel ffmpegHWAccelType = "cuda"
16+
)
17+
18+
func cudaOptions(path string, start time.Duration, index int) []string {
19+
mapValue := fmt.Sprintf("%d:v:0", index)
20+
cudaOptions := []string{"-hwaccel", "cuda", "-hwaccel_output_format", "cuda"}
21+
inputOptions := []string{"-ss", timeutil.String(start), "-i", path}
22+
filterOptions := []string{"-vf", "scale_cuda=256:-1", "-frames:v", "1"}
23+
encoderOptions := []string{"-c:v", "h264_nvenc"}
24+
outputOptions := []string{"-an", "-f", "rawvideo", "-map", mapValue, "pipe:1"}
25+
26+
return slices.Concat(cudaOptions, inputOptions, filterOptions, encoderOptions, outputOptions)
27+
}
28+
29+
func swOptions(path string, start time.Duration, index int) []string {
30+
mapValue := fmt.Sprintf("%d:v:0", index)
31+
inputOptions := []string{"-ss", timeutil.String(start), "-i", path}
32+
filterOptions := []string{"-vf", "scale=256:-2", "-frames:v", "1"}
33+
encoderOptions := []string{"-c:v", "libx264", "-preset", "ultrafast", "-tune", "zerolatency", "-crf", "0"}
34+
outputOptions := []string{"-an", "-f", "rawvideo", "-map", mapValue, "pipe:1"}
35+
36+
return slices.Concat(inputOptions, filterOptions, encoderOptions, outputOptions)
37+
}
38+
39+
func options(accel ffmpegHWAccelType) func(path string, start time.Duration, index int) []string {
40+
switch accel {
41+
case ffmpegCudaAccel:
42+
return cudaOptions
43+
case ffmpegSoftwareAccel:
44+
return swOptions
45+
default:
46+
return swOptions
47+
}
48+
}
49+
50+
func tileOptions() []string {
51+
quietOptions := []string{"-hide_banner", "-loglevel", "error"}
52+
inputOptions := []string{"-i", "pipe:0"}
53+
filterOptions := []string{"-c:v", "libwebp", "-vf", "tile=1x5", "-frames:v", "1"}
54+
outputOptions := []string{"-f", "image2", "-an", "pipe:1"}
55+
56+
return slices.Concat(quietOptions, inputOptions, filterOptions, outputOptions)
57+
}

pkg/preview/ffmpeg_prob.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package preview
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"context"
7+
"encoding/json"
8+
"errors"
9+
"fmt"
10+
"os/exec"
11+
"strconv"
12+
"strings"
13+
"time"
14+
15+
"github.com/hashicorp/go-version"
16+
)
17+
18+
var (
19+
ErrCommandNotFound = errors.New("not found")
20+
ErrIncorrectFFmpegVersion = errors.New("can't be parsed")
21+
ErrOutdatedFFmpegVersion = errors.New("is outdated")
22+
ErrMetaInfoUnmarshal = errors.New("failed to decode file's meta information")
23+
ErrMetaInfoFramesCountParse = errors.New("failed to parse frames count from meta information")
24+
ErrMetaInfoDurationParse = errors.New("failed to parse duration from meta information")
25+
ErrVideoStreamNotFound = errors.New("video stream not found")
26+
ErrParseFrameRate = errors.New("failed to parse frame rate")
27+
ErrNoSupportedCodecs = errors.New("there are no supported codecs")
28+
)
29+
30+
type probeFormat struct {
31+
Duration string `json:"duration"`
32+
}
33+
34+
type probeStream struct {
35+
Frames string `json:"nb_frames"` //nolint:tagliatelle
36+
Width int `json:"width"`
37+
Height int `json:"height"`
38+
AvgFrameRate string `json:"avg_frame_rate"` //nolint:tagliatelle
39+
CodecType string `json:"codec_type"` //nolint:tagliatelle
40+
CodecName string `json:"codec_nae"` //nolint:tagliatelle
41+
}
42+
43+
type probeData struct {
44+
Format probeFormat `json:"format"`
45+
Streams []probeStream `json:"streams"`
46+
}
47+
48+
func processorProbe() error {
49+
ctx := context.Background()
50+
51+
if err := probeFFmpeg(ctx, "ffmpeg"); err != nil {
52+
return err
53+
}
54+
55+
if err := probeFFmpeg(ctx, "ffprobe"); err != nil {
56+
return err
57+
}
58+
59+
return nil
60+
}
61+
62+
func formatCodecs(buff *bytes.Buffer) []string {
63+
codecs := []string{}
64+
scanner := bufio.NewScanner(buff)
65+
scanner.Split(bufio.ScanLines)
66+
67+
for scanner.Scan() {
68+
withoutPrefix := strings.TrimPrefix(scanner.Text(), "(codec ")
69+
codec := strings.TrimSuffix(withoutPrefix, ")")
70+
codecs = append(codecs, codec)
71+
}
72+
73+
return codecs
74+
}
75+
76+
func probeCuda(ctx context.Context) ([]string, error) {
77+
//nolint:lll
78+
cmd := exec.CommandContext(ctx, "bash", "-c", "ffprobe -hide_banner -decoders | grep Nvidia | grep -oP \"[(]codec\\s\\w+[)]\"")
79+
outBuff := bytes.NewBuffer(nil)
80+
errBuff := bytes.NewBuffer(nil)
81+
cmd.Stdout = outBuff
82+
cmd.Stderr = errBuff
83+
84+
if err := cmd.Run(); err != nil {
85+
return nil, fmt.Errorf("[%s] %w", errBuff.String(), err)
86+
}
87+
88+
if outBuff.Len() == 0 {
89+
return nil, fmt.Errorf("'%s' %w", "ffprobe -hide_banner -decoders", ErrCommandNotFound)
90+
}
91+
92+
supportedCudaCodecs := formatCodecs(outBuff)
93+
if len(supportedCudaCodecs) == 0 {
94+
return nil, ErrNoSupportedCodecs
95+
}
96+
97+
return supportedCudaCodecs, nil
98+
}
99+
100+
func probeFFmpeg(ctx context.Context, com string) error {
101+
cmd := exec.CommandContext(ctx, "bash", "-c", com+" -version | sed -n \"s/"+com+" version \\([-0-9.]*\\).*/\\1/p;\"") //nolint:gosec,lll
102+
buf := bytes.NewBuffer(nil)
103+
stdErrBuf := bytes.NewBuffer(nil)
104+
cmd.Stdout = buf
105+
cmd.Stderr = stdErrBuf
106+
107+
if err := cmd.Run(); err != nil {
108+
return fmt.Errorf("[%s] %w", stdErrBuf.String(), err)
109+
}
110+
111+
if buf.Len() == 0 {
112+
return fmt.Errorf("'%s' %w", com, ErrCommandNotFound)
113+
}
114+
115+
clearVersion := strings.TrimSuffix(strings.TrimSuffix(buf.String(), "\n"), "-0")
116+
required, _ := version.NewVersion("4.4.2")
117+
118+
existed, err := version.NewVersion(clearVersion)
119+
if err != nil {
120+
return fmt.Errorf("%w version(%s) of %s: %w", ErrIncorrectFFmpegVersion, buf.String(), com, err)
121+
}
122+
123+
if existed.LessThan(required) {
124+
return fmt.Errorf("%w version(%s) of %s: %w", ErrOutdatedFFmpegVersion, existed.String(), com, err)
125+
}
126+
127+
return nil
128+
}
129+
130+
func getVideoStream(streams []probeStream) *probeStream {
131+
for _, v := range streams {
132+
if v.CodecType == "video" {
133+
return &v
134+
}
135+
}
136+
137+
return nil
138+
}
139+
140+
type probeOutput struct {
141+
width int
142+
height int
143+
duration time.Duration
144+
codec string
145+
}
146+
147+
func probeOutputFrames(a string) (probeOutput, error) {
148+
data := probeData{}
149+
output := probeOutput{}
150+
151+
if err := json.Unmarshal([]byte(a), &data); err != nil {
152+
return output, fmt.Errorf("%w: %w", ErrMetaInfoUnmarshal, err)
153+
}
154+
155+
seconds, err := strconv.ParseFloat(data.Format.Duration, 64)
156+
if err != nil {
157+
return output, fmt.Errorf("%w: %w", ErrMetaInfoDurationParse, err)
158+
}
159+
160+
videoStream := getVideoStream(data.Streams)
161+
if videoStream == nil {
162+
return output, ErrVideoStreamNotFound
163+
}
164+
165+
output.width = videoStream.Width
166+
output.height = videoStream.Height
167+
output.duration = time.Duration(seconds) * time.Second
168+
output.codec = videoStream.CodecName
169+
170+
return output, nil
171+
}
172+
173+
func videoProbe(ctx context.Context, path string, timeOut time.Duration) (string, error) {
174+
if timeOut > 0 {
175+
var cancel func()
176+
ctx, cancel = context.WithTimeout(ctx, timeOut)
177+
defer cancel()
178+
}
179+
180+
logOptions := []string{"-hide_banner", "-loglevel", "quiet"}
181+
jobOptions := []string{"-show_format", "-show_streams", "-of", "json", path}
182+
options := []string{}
183+
options = append(options, logOptions...)
184+
options = append(options, jobOptions...)
185+
186+
cmd := exec.CommandContext(ctx, "ffprobe", options...)
187+
buf := bytes.NewBuffer(nil)
188+
stdErrBuf := bytes.NewBuffer(nil)
189+
cmd.Stdout = buf
190+
cmd.Stderr = stdErrBuf
191+
192+
if err := cmd.Run(); err != nil {
193+
return "", fmt.Errorf("[%s] %w", stdErrBuf.String(), err)
194+
}
195+
196+
return buf.String(), nil
197+
}

0 commit comments

Comments
 (0)