@@ -23,6 +23,7 @@ import (
23
23
"path/filepath"
24
24
"regexp"
25
25
"sort"
26
+ "strconv"
26
27
"strings"
27
28
"sync"
28
29
"time"
@@ -77,7 +78,7 @@ type Args struct {
77
78
FontBg * colors.Color `ox:"font background color,default:white"`
78
79
FontDPI int `ox:"font dpi,default:100,name:font-dpi"`
79
80
FontMargin int `ox:"margin,default:5"`
80
- TimeCode time.Duration `ox:"time code,short:t"`
81
+ TimeCode time.Duration `ox:"video time code,short:t"`
81
82
VipsConcurrency int `ox:"vips concurrency,default:$NUMCPU"`
82
83
83
84
ctx context.Context
@@ -299,6 +300,7 @@ func (args *Args) renderVips(r io.Reader, name string) (image.Image, error) {
299
300
func (args * Args ) renderFfmpeg (_ io.Reader , pathName string ) (image.Image , error ) {
300
301
var err error
301
302
ffmpegOnce .Do (func () {
303
+ ffprobePath , _ = exec .LookPath ("ffprobe" )
302
304
ffmpegPath , err = exec .LookPath ("ffmpeg" )
303
305
})
304
306
switch {
@@ -310,7 +312,7 @@ func (args *Args) renderFfmpeg(_ io.Reader, pathName string) (image.Image, error
310
312
// ffmpeg -loglevel panic -hide_banner -ss $t -i *.mkv -vframes 1 -q:v 1
311
313
params := []string {
312
314
`-hide_banner` ,
313
- // `-ss`, `` ,
315
+ `-ss` , args . ffprobeTimecode ( pathName ) ,
314
316
`-i` , pathName ,
315
317
`-vframes` , `1` ,
316
318
`-q:v` , `1` ,
@@ -337,6 +339,62 @@ func (args *Args) renderFfmpeg(_ io.Reader, pathName string) (image.Image, error
337
339
return png .Decode (& buf )
338
340
}
339
341
342
+ func (args * Args ) ffprobeTimecode (pathName string ) string {
343
+ switch {
344
+ case ffprobePath == "" :
345
+ return "00:00"
346
+ case args .TimeCode != 0 :
347
+ return formatTimecode (args .TimeCode )
348
+ }
349
+ params := []string {
350
+ `-loglevel` , `quiet` ,
351
+ `-show_format` ,
352
+ pathName ,
353
+ }
354
+ args .logger ("ffprobe: executing %s %s" , ffprobePath , strings .Join (params , " " ))
355
+ cmd := exec .CommandContext (args .ctx , ffprobePath , params ... )
356
+ buf , err := cmd .CombinedOutput ()
357
+ if err != nil {
358
+ return "00:00"
359
+ }
360
+ m := durationRE .FindStringSubmatch (string (buf ))
361
+ if m == nil {
362
+ return "00:00"
363
+ }
364
+ f , err := strconv .ParseFloat (m [1 ], 64 )
365
+ if err != nil {
366
+ return "00:00"
367
+ }
368
+ switch dur := time .Duration (f * float64 (time .Second )); {
369
+ case dur >= 1 * time .Hour :
370
+ return "10:00"
371
+ case dur >= 30 * time .Minute :
372
+ return "05:00"
373
+ case dur >= 15 * time .Minute :
374
+ return "03:00"
375
+ case dur >= 5 * time .Minute :
376
+ return "02:00"
377
+ case dur > 1 * time .Minute :
378
+ return "00:30"
379
+ case dur > 30 * time .Second :
380
+ return "00:10"
381
+ case dur > 5 * time .Second :
382
+ return "00:02"
383
+ }
384
+ return "00:00"
385
+ }
386
+
387
+ var durationRE = regexp .MustCompile (`(?m)^duration=(.*)$` )
388
+
389
+ func formatTimecode (d time.Duration ) string {
390
+ if d == 0 {
391
+ return "00:00"
392
+ }
393
+ secs := int64 (float64 (d ) / float64 (time .Minute ))
394
+ rem := int64 ((float64 (d ) / float64 (time .Minute )) * float64 (time .Minute ))
395
+ return fmt .Sprintf ("%02d:%02d" , secs , rem )
396
+ }
397
+
340
398
// renderLibreOffice renders the image using the `soffice` command.
341
399
func (args * Args ) renderLibreOffice (_ io.Reader , pathName string ) (image.Image , error ) {
342
400
var err error
@@ -706,6 +764,7 @@ var (
706
764
707
765
var (
708
766
sofficePath string
767
+ ffprobePath string
709
768
ffmpegPath string
710
769
)
711
770
0 commit comments