Skip to content

Commit 027b328

Browse files
committed
Added methods for thumbnails
1 parent a9f32d4 commit 027b328

File tree

6 files changed

+133
-0
lines changed

6 files changed

+133
-0
lines changed

cmd/cli/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type CLI struct {
3535
Artwork ArtworkCmd `cmd:"artwork" help:"Save artwork from media file"`
3636
Probe ProbeCmd `cmd:"probe" help:"Probe media file or device"`
3737
Decode DecodeCmd `cmd:"decode" help:"Decode media"`
38+
Thumbnails ThumbnailsCmd `cmd:"thumbnails" help:"Generate thumbnails from media file"`
3839
}
3940

4041
func main() {

cmd/cli/thumbnails.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"image/png"
8+
"io/fs"
9+
"os"
10+
"path/filepath"
11+
"time"
12+
13+
// Packages
14+
"github.com/mutablelogic/go-media"
15+
"github.com/mutablelogic/go-media/pkg/file"
16+
)
17+
18+
type ThumbnailsCmd struct {
19+
Path string `arg:"" required:"" help:"Media file or path" type:"path"`
20+
Dur time.Duration `name:"duration" help:"Duration between thumnnails" type:"duration" default:"1m"`
21+
Width int `name:"width" help:"Width of thumbnail" type:"int" default:"320"`
22+
}
23+
24+
func (cmd *ThumbnailsCmd) Run(globals *Globals) error {
25+
// Create the walker with the processor callback
26+
walker := file.NewWalker(func(ctx context.Context, root, relpath string, info fs.FileInfo) error {
27+
if info.IsDir() || info.Size() == 0 {
28+
return nil
29+
}
30+
if err := cmd.mediaWalker(ctx, globals.manager, filepath.Join(root, relpath)); err != nil {
31+
if err == context.Canceled {
32+
globals.manager.Infof("Cancelled\n")
33+
} else {
34+
globals.manager.Errorf("Error processing %q: %v\n", relpath, err)
35+
}
36+
}
37+
return nil
38+
})
39+
40+
// Walk the filesystem
41+
return walker.Walk(globals.ctx, cmd.Path)
42+
}
43+
44+
func (cmd *ThumbnailsCmd) mediaWalker(ctx context.Context, manager media.Manager, path string) error {
45+
reader, err := manager.Open(path, nil)
46+
if err != nil {
47+
return err
48+
}
49+
defer reader.Close()
50+
51+
// Create a decoder for video - output 320x240 frames
52+
// We should adjust this to match the output size of the thumbnail
53+
decoder, err := reader.Decoder(func(stream media.Stream) (media.Parameters, error) {
54+
if stream.Type().Is(media.VIDEO) {
55+
// TODO: We need to use the sample aspect ratio
56+
width := cmd.Width
57+
height := stream.Parameters().Height() * width / stream.Parameters().Width()
58+
return manager.VideoParameters(width, height, "rgb24")
59+
} else {
60+
return nil, nil
61+
}
62+
})
63+
if err != nil {
64+
return err
65+
}
66+
67+
// Decode the frames
68+
var t time.Duration = -1
69+
return decoder.Decode(ctx, func(frame media.Frame) error {
70+
// Logic to return if we have a frame within the duration
71+
if frame.Time() < 0 {
72+
return nil
73+
} else if t != -1 && frame.Time()-t < cmd.Dur {
74+
return nil
75+
} else {
76+
t = frame.Time()
77+
}
78+
79+
// Save the frame
80+
filename := fmt.Sprintf("%s.%s.png", filepath.Base(path), t.Truncate(time.Second))
81+
image, err := frame.Image()
82+
if err != nil {
83+
return err
84+
}
85+
86+
w, err := os.Create(filename)
87+
if err != nil {
88+
return err
89+
}
90+
defer w.Close()
91+
92+
fmt.Println("Writing", filename)
93+
if err := png.Encode(w, image); err != nil {
94+
return errors.Join(err, os.Remove(filename))
95+
}
96+
97+
return nil
98+
})
99+
}

decoder.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ func newDemuxer(input *ff.AVFormatContext, mapfn DecoderMapFunc, force bool) (*d
7777
return nil, errors.Join(result, demuxer.close())
7878
}
7979

80+
// If no streams were selected, return an error
81+
if len(demuxer.decoders) == 0 {
82+
return nil, errors.Join(demuxer.close(), errors.New("no streams to decode"))
83+
}
84+
8085
// Create a frame for encoding - after resampling and resizing
8186
if frame := ff.AVUtil_frame_alloc(); frame == nil {
8287
return nil, errors.Join(demuxer.close(), errors.New("failed to allocate frame"))

frame.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package media
33
import (
44
"encoding/json"
55
"image"
6+
"time"
67

78
// Packages
89
imagex "github.com/mutablelogic/go-media/pkg/image"
@@ -70,6 +71,15 @@ func (frame *frame) Type() MediaType {
7071
return NONE
7172
}
7273

74+
// Return the timestamp as a duration, or minus one if not set
75+
func (frame *frame) Time() time.Duration {
76+
pts := frame.ctx.Pts()
77+
if pts == ff.AV_NOPTS_VALUE {
78+
return -1
79+
}
80+
return secondsToDuration(float64(pts) * ff.AVUtil_q2d(frame.ctx.TimeBase()))
81+
}
82+
7383
// Return the number of planes for a specific PixelFormat
7484
// or SampleFormat and ChannelLayout combination
7585
func (frame *frame) NumPlanes() int {
@@ -213,3 +223,10 @@ func (frame *frame) PixelFormat() string {
213223
}
214224
return ff.AVUtil_get_pix_fmt_name(frame.ctx.PixFmt())
215225
}
226+
227+
////////////////////////////////////////////////////////////////////////////////
228+
// PRIVATE METHODS
229+
230+
func secondsToDuration(seconds float64) time.Duration {
231+
return time.Duration(seconds * float64(time.Second))
232+
}

interfaces.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"context"
1010
"image"
1111
"io"
12+
"time"
1213
)
1314

1415
// Manager represents a manager for media formats and devices.
@@ -257,6 +258,10 @@ type Frame interface {
257258
// Return a frame plane as a byte slice.
258259
Bytes(int) []byte
259260

261+
// Return the presentation timestamp for the frame or
262+
// a negative number if not set
263+
Time() time.Duration
264+
260265
// Return a frame as an image, which supports the following
261266
// pixel formats: AV_PIX_FMT_GRAY8, AV_PIX_FMT_RGBA,
262267
// AV_PIX_FMT_RGB24, AV_PIX_FMT_YUV420P

sys/ffmpeg61/avutil_rational.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,15 @@ func (r AVRational) Float(multiplier int64) float64 {
6060
////////////////////////////////////////////////////////////////////////////////
6161
// BINDINGS
6262

63+
// Convert a double precision floating point number to a rational.
6364
func AVUtil_rational_d2q(d float64, max int) AVRational {
6465
if max == 0 {
6566
max = C.INT_MAX
6667
}
6768
return AVRational(C.av_d2q(C.double(d), C.int(max)))
6869
}
70+
71+
// Convert an AVRational to a double.
72+
func AVUtil_q2d(a AVRational) float64 {
73+
return float64(C.av_q2d(C.AVRational(a)))
74+
}

0 commit comments

Comments
 (0)