Skip to content

Commit 18e78fc

Browse files
committed
Updates
1 parent cac61ac commit 18e78fc

File tree

5 files changed

+121
-57
lines changed

5 files changed

+121
-57
lines changed

README.md

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -108,53 +108,82 @@ func main() {
108108

109109
### Decoding - Video
110110

111-
This example shows you how to decode video frames from a media file into images.
111+
This example shows you how to decode video frames from a media file into images, and
112+
encode those images to JPEG format.
112113

113114
```go
114115
package main
115116

116117
import (
117-
media "github.com/mutablelogic/go-media"
118+
"context"
119+
"fmt"
120+
"image/jpeg"
121+
"io"
122+
"log"
123+
"os"
124+
"path/filepath"
125+
126+
// Packages
127+
ffmpeg "github.com/mutablelogic/go-media/pkg/ffmpeg"
128+
129+
// Namespace imports
130+
. "github.com/mutablelogic/go-media"
118131
)
119132

120133
func main() {
121-
manager, err := media.NewManager()
134+
// Open a media file for reading. The format of the file is guessed.
135+
input, err := ffmpeg.Open(os.Args[1])
122136
if err != nil {
123137
log.Fatal(err)
124138
}
125139

126-
media, err := manager.Open("etc/test/sample.mp4", nil)
127-
if err != nil {
128-
log.Fatal(err)
140+
// Make a map function which can be used to decode the streams and set
141+
// the parameters we want from the decode. The audio and video streams
142+
// are resampled and resized to fit the parameters we pass back the decoder.
143+
mapfunc := func(stream int, par *ffmpeg.Par) (*ffmpeg.Par, error) {
144+
if par.Type() == VIDEO {
145+
// Convert frame to yuv420p to frame size and rate as source
146+
return ffmpeg.VideoPar("yuv420p", par.WidthHeight(), par.FrameRate()), nil
147+
}
148+
// Ignore other streams
149+
return nil, nil
129150
}
130-
defer media.Close()
131151

132-
// Create a decoder for the media file. Only video streams are decoded
133-
decoder, err := media.Decoder(func(stream Stream) (Parameters, error) {
134-
if stream.Type() == VIDEO {
135-
// Copy video
136-
return stream.Parameters(), nil
137-
} else {
138-
// Ignore other stream types
139-
return nil, nil
140-
}
141-
})
152+
// Make a folder where we're going to store the thumbnails
153+
tmp, err := os.MkdirTemp("", "decode")
142154
if err != nil {
143155
log.Fatal(err)
144156
}
145157

146-
// The frame function is called for each frame in the stream
147-
framefn := func(frame Frame) error {
148-
image, err := frame.Image()
158+
// Decode the streams and receive the video frame
159+
// If the map function is nil, the frames are copied. In this example,
160+
// we get a yuv420p frame at the same size as the original.
161+
n := 0
162+
err = input.Decode(context.Background(), mapfunc, func(stream int, frame *ffmpeg.Frame) error {
163+
// Write the frame to a file
164+
w, err := os.Create(filepath.Join(tmp, fmt.Sprintf("frame-%d-%d.jpg", stream, n)))
149165
if err != nil {
150166
return err
151167
}
152-
// TODO: Do something with the image here....
153-
return nil
154-
}
168+
defer w.Close()
169+
170+
// Convert to an image and encode a JPEG
171+
if image, err := frame.Image(); err != nil {
172+
return err
173+
} else if err := jpeg.Encode(w, image, nil); err != nil {
174+
return err
175+
} else {
176+
log.Println("Wrote:", w.Name())
177+
}
155178

156-
// decode frames from the stream
157-
if err := decoder.Decode(context.Background(), framefn); err != nil {
179+
// End after 10 frames
180+
n++
181+
if n >= 10 {
182+
return io.EOF
183+
}
184+
return nil
185+
})
186+
if err != nil {
158187
log.Fatal(err)
159188
}
160189
}

cmd/examples/decode/main.go

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,49 +2,75 @@ package main
22

33
import (
44
"context"
5+
"fmt"
6+
"image/jpeg"
7+
"io"
58
"log"
69
"os"
10+
"path/filepath"
711

8-
media "github.com/mutablelogic/go-media"
12+
// Packages
13+
ffmpeg "github.com/mutablelogic/go-media/pkg/ffmpeg"
14+
15+
// Namespace imports
16+
. "github.com/mutablelogic/go-media"
917
)
1018

1119
func main() {
12-
manager, err := media.NewManager()
20+
// Open a media file for reading. The format of the file is guessed.
21+
input, err := ffmpeg.Open(os.Args[1])
1322
if err != nil {
1423
log.Fatal(err)
1524
}
1625

17-
// Open a media file for reading. The format of the file is guessed.
18-
// Alteratively, you can pass a format as the second argument. Further optional
19-
// arguments can be used to set the format options.
20-
file, err := manager.Open(os.Args[1], nil)
21-
if err != nil {
22-
log.Fatal(err)
26+
// Make a map function which can be used to decode the streams and set
27+
// the parameters we want each audio and video stream to have.
28+
// The audio and video streams are resampled and resized to fit the
29+
// parameters we pass to the decoder.
30+
mapfunc := func(stream int, par *ffmpeg.Par) (*ffmpeg.Par, error) {
31+
if par.Type() == VIDEO {
32+
// Convert frame to yuv420p as needed
33+
return ffmpeg.VideoPar("yuv420p", par.WidthHeight(), par.FrameRate()), nil
34+
}
35+
// Ignore other streams
36+
return nil, nil
2337
}
24-
defer file.Close()
25-
26-
// Choose which streams to demultiplex - pass the stream parameters
27-
// to the decoder. If you don't want to resample or reformat the streams,
28-
// then you can pass nil as the function and all streams will be demultiplexed.
29-
decoder, err := file.Decoder(func(stream media.Stream) (media.Parameters, error) {
30-
// Copy streams, don't resample or resize
31-
return stream.Parameters(), nil
32-
})
38+
39+
// Make a folder where we're going to store the thumbnails
40+
tmp, err := os.MkdirTemp("", "decode")
3341
if err != nil {
3442
log.Fatal(err)
3543
}
3644

37-
// Demuliplex the stream and receive the frames of audio and video.
38-
if err := decoder.Decode(context.Background(), func(frame media.Frame) error {
39-
// Each packet is specific to a stream. It can be processed here
40-
// to receive audio or video frames, then resize or resample them,
41-
// for example. Alternatively, you can pass the packet to an encoder
42-
// to remultiplex the streams without processing them.
43-
log.Print(frame)
45+
// Decode the streams and receive the video frame
46+
// If the map function is nil, the frames are copied. In this example,
47+
// we get a yuv420p frame at the same size as the original.
48+
n := 0
49+
err = input.Decode(context.Background(), mapfunc, func(stream int, frame *ffmpeg.Frame) error {
50+
// Write the frame to a file
51+
w, err := os.Create(filepath.Join(tmp, fmt.Sprintf("frame-%d.jpg", n)))
52+
if err != nil {
53+
return err
54+
}
55+
defer w.Close()
56+
57+
// Coovert to an image and encode a JPEG
58+
if image, err := frame.Image(); err != nil {
59+
return err
60+
} else if err := jpeg.Encode(w, image, nil); err != nil {
61+
return err
62+
} else {
63+
log.Println("Wrote:", w.Name())
64+
}
4465

45-
// Return io.EOF to stop processing, nil to continue
66+
// End after 10 frames
67+
n++
68+
if n >= 10 {
69+
return io.EOF
70+
}
4671
return nil
47-
}); err != nil {
72+
})
73+
if err != nil {
4874
log.Fatal(err)
4975
}
5076
}

pkg/ffmpeg/par.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package ffmpeg
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"slices"
67

78
// Packages
@@ -72,8 +73,8 @@ func NewVideoPar(pixfmt string, size string, framerate float64) (*Par, error) {
7273
}
7374

7475
// Frame rate
75-
if framerate <= 0 {
76-
return nil, ErrBadParameter.Withf("negative or zero framerate %v", framerate)
76+
if framerate < 0 {
77+
return nil, ErrBadParameter.Withf("negative framerate %v", framerate)
7778
} else {
7879
par.SetFramerate(ff.AVUtil_rational_d2q(framerate, 1<<24))
7980
}
@@ -146,6 +147,14 @@ func (ctx *Par) Type() media.Type {
146147
}
147148
}
148149

150+
func (ctx *Par) WidthHeight() string {
151+
return fmt.Sprintf("%dx%d", ctx.Width(), ctx.Height())
152+
}
153+
154+
func (ctx *Par) FrameRate() float64 {
155+
return ff.AVUtil_rational_q2d(ctx.Framerate())
156+
}
157+
149158
func (ctx *Par) ValidateFromCodec(codec *ff.AVCodec) error {
150159
switch codec.Type() {
151160
case ff.AVMEDIA_TYPE_AUDIO:

pkg/ffmpeg/reader.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ func (r *Reader) Metadata(keys ...string) []*Metadata {
204204
// The decoding can be interrupted by cancelling the context, or by the decodefn
205205
// returning an error or io.EOF. The latter will end the decoding process early but
206206
// will not return an error.
207-
func (r *Reader) Decode(ctx context.Context, decodefn DecoderFrameFn, mapfn DecoderMapFunc) error {
207+
func (r *Reader) Decode(ctx context.Context, mapfn DecoderMapFunc, decodefn DecoderFrameFn) error {
208208
decoders := make(map[int]*Decoder, r.input.NumStreams())
209209

210210
// Standard decoder map function copies all streams

pkg/ffmpeg/reader_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func Test_reader_003(t *testing.T) {
7171
return nil
7272
}
7373

74-
if err := media.Decode(context.Background(), framefn, nil); !assert.NoError(err) {
74+
if err := media.Decode(context.Background(), nil, framefn); !assert.NoError(err) {
7575
t.FailNow()
7676
}
7777
}
@@ -104,7 +104,7 @@ func Test_reader_004(t *testing.T) {
104104
return nil
105105
}
106106

107-
if err := media.Decode(context.Background(), framefn, mapfn); !assert.NoError(err) {
107+
if err := media.Decode(context.Background(), mapfn, framefn); !assert.NoError(err) {
108108
t.FailNow()
109109
}
110110
}
@@ -163,7 +163,7 @@ func Test_reader_005(t *testing.T) {
163163
return nil
164164
}
165165

166-
if err := input.Decode(context.Background(), framefn, mapfn); !assert.NoError(err) {
166+
if err := input.Decode(context.Background(), mapfn, framefn); !assert.NoError(err) {
167167
t.FailNow()
168168
}
169169
}

0 commit comments

Comments
 (0)