diff --git a/cmd/examples/sdlplayer/context.go b/cmd/examples/sdlplayer/context.go new file mode 100644 index 0000000..97c8708 --- /dev/null +++ b/cmd/examples/sdlplayer/context.go @@ -0,0 +1,33 @@ +package main + +import ( + "context" + "os" + "os/signal" +) + +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +// ContextForSignal returns a context object which is cancelled when a signal +// is received. It returns nil if no signal parameter is provided +func ContextForSignal(signals ...os.Signal) context.Context { + if len(signals) == 0 { + return nil + } + + ch := make(chan os.Signal, 1) + ctx, cancel := context.WithCancel(context.Background()) + + // Send message on channel when signal received + signal.Notify(ch, signals...) + + // When any signal received, call cancel + go func() { + <-ch + cancel() + }() + + // Return success + return ctx +} diff --git a/cmd/examples/sdlplayer/main.go b/cmd/examples/sdlplayer/main.go new file mode 100644 index 0000000..391b353 --- /dev/null +++ b/cmd/examples/sdlplayer/main.go @@ -0,0 +1,36 @@ +/* This example demonstrates how to play audio and video files using SDL2. */ +package main + +import ( + "errors" + "fmt" + "os" + "syscall" +) + +func main() { + // Bail out when we receive a signal + ctx := ContextForSignal(os.Interrupt, syscall.SIGQUIT) + + // Create a player object + player := NewPlayer() + defer player.Close() + + // Open the file + var result error + if len(os.Args) == 2 { + result = player.OpenUrl(os.Args[1]) + } else { + result = errors.New("usage: sdlplayer ") + } + if result != nil { + fmt.Fprintln(os.Stderr, result) + os.Exit(-1) + } + + // Play + if err := player.Play(ctx); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(-1) + } +} diff --git a/cmd/examples/sdlplayer/player.go b/cmd/examples/sdlplayer/player.go new file mode 100644 index 0000000..1e3dec2 --- /dev/null +++ b/cmd/examples/sdlplayer/player.go @@ -0,0 +1,212 @@ +package main + +import ( + + // Packages + "context" + "errors" + "fmt" + "os" + "sync" + "unsafe" + + "github.com/mutablelogic/go-media/pkg/ffmpeg" + "github.com/mutablelogic/go-media/pkg/sdl" + + // Namespace imports + . "github.com/mutablelogic/go-media" +) + +type Player struct { + input *ffmpeg.Reader + ctx *ffmpeg.Context + audio *ffmpeg.Par + video *ffmpeg.Par + videoevent uint32 + audioevent uint32 +} + +func NewPlayer() *Player { + return &Player{} +} + +func (p *Player) Close() error { + var result error + + // Close resources + if p.ctx != nil { + result = errors.Join(result, p.ctx.Close()) + } + if p.input != nil { + result = errors.Join(result, p.input.Close()) + } + + // Return any errors + return result +} + +func (p *Player) OpenUrl(url string) error { + input, err := ffmpeg.Open(url) + if err != nil { + return err + } + p.input = input + + // Map input streams - find best audio and video streams + ctx, err := p.input.Map(func(stream int, par *ffmpeg.Par) (*ffmpeg.Par, error) { + if stream == p.input.BestStream(VIDEO) { + p.video = par + return par, nil + } else if stream == p.input.BestStream(AUDIO) { + p.audio = par + return par, nil + } else { + return nil, nil + } + }) + if err != nil { + return err + } else { + p.ctx = ctx + p.input = input + } + return nil +} + +func (p *Player) Type() Type { + t := NONE + if p.video != nil { + t |= VIDEO + } + if p.audio != nil { + t |= AUDIO + } + return t +} + +// Return media title +func (p *Player) Title() string { + title := p.input.Metadata("title") + if len(title) > 0 { + return title[0].Value() + } + return fmt.Sprint(p.Type()) +} + +func (p *Player) Play(ctx context.Context) error { + var window *sdl.Window + var audio *sdl.Audio + + // Create a new SDL context + sdl, err := sdl.New(p.Type()) + if err != nil { + return err + } + defer sdl.Close() + + // Create a window for video + if p.video != nil { + if w, err := sdl.NewVideo(p.Title(), p.video); err != nil { + return err + } else { + window = w + } + defer window.Close() + + // Register a method to push video rendering + p.videoevent = sdl.Register(func(frame unsafe.Pointer) { + var result error + frame_ := (*ffmpeg.Frame)(frame) + if err := window.RenderFrame(frame_); err != nil { + result = errors.Join(result, err) + } + if err := window.Flush(); err != nil { + result = errors.Join(result, err) + } + if err := frame_.Close(); err != nil { + result = errors.Join(result, err) + } + if result != nil { + fmt.Fprintln(os.Stderr, result) + } + /* + // Pause to present the frame at the correct PTS + if pts != ffmpeg.TS_UNDEFINED && pts < frame.Ts() { + pause := frame.Ts() - pts + if pause > 0 { + sdl.Delay(uint32(pause * 1000)) + } + } + + // Set current timestamp + pts = frame.Ts() + + // Render the frame, release the frame resources + if err := w.RenderFrame(frame); err != nil { + log.Print(err) + } else if err := w.Flush(); err != nil { + log.Print(err) + } else if err := frame.Close(); err != nil { + log.Print(err) + } + */ + }) + } + + // Create audio + if p.audio != nil { + if a, err := sdl.NewAudio(p.audio); err != nil { + return err + } else { + audio = a + } + defer audio.Close() + + // Register a method to push audio rendering + p.audioevent = sdl.Register(func(frame unsafe.Pointer) { + //frame_ := (*ffmpeg.Frame)(frame) + //fmt.Println("TODO: Audio", frame_) + }) + } + + // Start go routine to decode the audio and video frames + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + if err := p.decode(ctx, sdl); err != nil { + fmt.Fprintln(os.Stderr, err) + } + }() + + // Run loop with events for audio and video + var result error + if err := sdl.Run(ctx); err != nil { + result = err + } + + // Wait for go routines to finish + wg.Wait() + + // Return any errors + return result +} + +// Goroutine decoder +func (p *Player) decode(ctx context.Context, sdl *sdl.Context) error { + return p.input.DecodeWithContext(ctx, p.ctx, func(stream int, frame *ffmpeg.Frame) error { + if frame.Type().Is(VIDEO) { + if copy, err := frame.Copy(); err != nil { + fmt.Println("Unable to make a frame copy: ", err) + } else { + // TODO: Make a copy of the frame + sdl.Post(p.videoevent, unsafe.Pointer(copy)) + } + } + if frame.Type().Is(AUDIO) { + // TODO: Make a copy of the frame + sdl.Post(p.audioevent, unsafe.Pointer(frame)) + } + return nil + }) +} diff --git a/cmd/examples/transcode/main.go b/cmd/examples/transcode/main.go index 319dfd4..ada0681 100644 --- a/cmd/examples/transcode/main.go +++ b/cmd/examples/transcode/main.go @@ -1,8 +1,12 @@ package main import ( + "errors" + "fmt" "log" "os" + "sync" + "syscall" // Packages ffmpeg "github.com/mutablelogic/go-media/pkg/ffmpeg" @@ -10,6 +14,9 @@ import ( // This example encodes an audio an video stream to a file func main() { + // Bail out when we receive a signal + ctx := ContextForSignal(os.Interrupt, syscall.SIGQUIT) + // Check we have a filename if len(os.Args) != 3 { log.Fatal("Usage: transcode ") @@ -21,4 +28,70 @@ func main() { log.Fatal(err) } defer in.Close() + + // Map to output file + decoding, err := in.Map(func(stream int, par *ffmpeg.Par) (*ffmpeg.Par, error) { + // This is where you specify the output format for the input stream + return par, nil + }) + if err != nil { + log.Fatal(err) + } + + // Create an output file from from the map + out, err := ffmpeg.Create(os.Args[2], ffmpeg.OptContext(decoding)) + if err != nil { + log.Fatal(err) + } + defer out.Close() + + // Decoding goroutine + var result error + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + + // This is where we decode the stream + result = errors.Join(result, in.DecodeWithContext(ctx, decoding, func(stream int, frame *ffmpeg.Frame) error { + + // Add the frame onto the encoding queue + fmt.Println("->DECODE:", stream, frame) + decoding.C(stream) <- frame + fmt.Println("<-DECODE") + + // Return success + return nil + })) + }() + + // Encoding goroutine + wg.Add(1) + go func() { + defer wg.Done() + + // This is where we encode the stream + result = errors.Join(result, out.Encode(ctx, func(stream int) (*ffmpeg.Frame, error) { + fmt.Println("->ENCODE", stream) + select { + case frame := <-decoding.C(stream): + fmt.Println("<-ENCODE", frame) + return frame, nil + default: + // Not ready to pass a frame back + fmt.Println("<-ENCODE", nil) + return nil, nil + } + }, nil)) + if result != nil { + fmt.Println("ERROR:", result) + } + }() + + wg.Wait() + + if result != nil { + log.Fatal(result) + } + fmt.Println("Transcoded to", out) } diff --git a/pkg/ffmpeg/context.go b/pkg/ffmpeg/context.go new file mode 100644 index 0000000..fbf6ae8 --- /dev/null +++ b/pkg/ffmpeg/context.go @@ -0,0 +1,171 @@ +package ffmpeg + +import ( + "context" + "errors" + "io" + "syscall" + + // Packages + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" + + // Namespace imports + . "github.com/djthorpe/go-errors" +) + +//////////////////////////////////////////////////////////////////////////////// +// TYPES + +// Decoding context +type Context struct { + input *ff.AVFormatContext + decoders map[int]*Decoder + ch map[int]chan *Frame +} + +//////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +// Create a new decoding context +func newContext(r *Reader, fn DecoderMapFunc) (*Context, error) { + ctx := new(Context) + ctx.input = r.input + ctx.decoders = make(map[int]*Decoder, r.input.NumStreams()) + ctx.ch = make(map[int]chan *Frame, r.input.NumStreams()) + + // Do stream mapping + if err := ctx.mapStreams(fn, r.force); err != nil { + return nil, errors.Join(err, ctx.Close()) + } + + // Make channels for each decoder + for stream_index := range ctx.decoders { + ctx.ch[stream_index] = make(chan *Frame) + } + + // Return sucess + return ctx, nil +} + +// Release resources for the decoding context +func (c *Context) Close() error { + var result error + for _, decoder := range c.decoders { + if err := decoder.Close(); err != nil { + result = errors.Join(result, err) + } + } + for _, ch := range c.ch { + close(ch) + } + + // Release resources + c.decoders = nil + c.ch = nil + c.input = nil + + // Return any errors + return result +} + +//////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func (decoder *Context) C(stream int) chan *Frame { + return decoder.ch[stream] +} + +//////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +func (decoder *Context) decode(ctx context.Context, fn DecoderFrameFn) error { + // Allocate a packet + packet := ff.AVCodec_packet_alloc() + if packet == nil { + return errors.New("failed to allocate packet") + } + defer ff.AVCodec_packet_free(packet) + + // Read packets +FOR_LOOP: + for { + select { + case <-ctx.Done(): + break FOR_LOOP + default: + if err := ff.AVFormat_read_frame(decoder.input, packet); errors.Is(err, io.EOF) { + break FOR_LOOP + } else if errors.Is(err, syscall.EAGAIN) { + continue FOR_LOOP + } else if err != nil { + return ErrInternalAppError.With("AVFormat_read_frame: ", err) + } + stream_index := packet.StreamIndex() + if decoder := decoder.decoders[stream_index]; decoder != nil { + if err := decoder.decode(packet, fn); errors.Is(err, io.EOF) { + break FOR_LOOP + } else if err != nil { + return err + } + } + } + + // Unreference the packet + ff.AVCodec_packet_unref(packet) + } + + // Flush the decoders + for _, decoder := range decoder.decoders { + if err := decoder.decode(nil, fn); errors.Is(err, io.EOF) { + // no-op + } else if err != nil { + return err + } + } + + // Return the context error - will be cancelled, perhaps, or nil if the + // demuxer finished successfully without cancellation + return ctx.Err() +} + +// Map streams to decoders, and return the decoders +func (c *Context) mapStreams(fn DecoderMapFunc, force bool) error { + // Standard decoder map function copies all streams + if fn == nil { + fn = func(_ int, par *Par) (*Par, error) { + return par, nil + } + } + + // Create a decoder for each stream. The decoder map function + // should be returning the parameters for the destination frame. + var result error + for _, stream := range c.input.Streams() { + stream_index := stream.Index() + + // Get decoder parameters and map to a decoder + par, err := fn(stream_index, &Par{ + AVCodecParameters: *stream.CodecPar(), + timebase: stream.TimeBase(), + }) + if err != nil { + result = errors.Join(result, err) + } else if par == nil { + continue + } else if decoder, err := NewDecoder(stream, par, force); err != nil { + result = errors.Join(result, err) + } else if _, exists := c.decoders[stream_index]; exists { + result = errors.Join(result, ErrDuplicateEntry.Withf("stream index %d", stream_index)) + } else { + c.decoders[stream_index] = decoder + } + } + + // Check to see if we have to do something + if len(c.decoders) == 0 { + result = errors.Join(result, ErrBadParameter.With("no streams to decode")) + } + + // Return any errors + return result +} diff --git a/pkg/ffmpeg/decoder.go b/pkg/ffmpeg/decoder.go index f709be5..d92f4b2 100644 --- a/pkg/ffmpeg/decoder.go +++ b/pkg/ffmpeg/decoder.go @@ -31,7 +31,7 @@ type Decoder struct { // Create a stream decoder which can decode packets from the input stream func NewDecoder(stream *ff.AVStream, dest *Par, force bool) (*Decoder, error) { decoder := new(Decoder) - decoder.stream = stream.Id() + decoder.stream = stream.Index() decoder.par = dest decoder.timeBase = stream.TimeBase() diff --git a/pkg/ffmpeg/opts.go b/pkg/ffmpeg/opts.go index 60eb761..003defb 100644 --- a/pkg/ffmpeg/opts.go +++ b/pkg/ffmpeg/opts.go @@ -131,6 +131,28 @@ func OptStream(stream int, par *Par) Opt { } } +// New streams with parameters from the context +func OptContext(context *Context) Opt { + return func(o *opts) error { + if context == nil { + return ErrBadParameter.With("invalid decoding context") + } + for stream, decoder := range context.decoders { + if _, exists := o.streams[stream]; exists { + return ErrDuplicateEntry.Withf("stream %v", stream) + } + if stream < 0 { + return ErrBadParameter.Withf("invalid stream %v", stream) + } + o.streams[stream] = decoder.par + } + + // Return success + return nil + } + +} + // Force resampling and resizing on decode, even if the input and output // parameters are the same func OptForce() Opt { diff --git a/pkg/ffmpeg/reader.go b/pkg/ffmpeg/reader.go index 55f5042..2a7f21b 100644 --- a/pkg/ffmpeg/reader.go +++ b/pkg/ffmpeg/reader.go @@ -7,15 +7,11 @@ import ( "io" "slices" "strings" - "syscall" "time" // Packages media "github.com/mutablelogic/go-media" ff "github.com/mutablelogic/go-media/sys/ffmpeg61" - - // Namespace imports - . "github.com/djthorpe/go-errors" ) //////////////////////////////////////////////////////////////////////////////// @@ -23,10 +19,11 @@ import ( // Media reader which reads from a URL, file path or device type Reader struct { - t media.Type - input *ff.AVFormatContext - avio *ff.AVIOContextEx - force bool + t media.Type + input *ff.AVFormatContext + avio *ff.AVIOContextEx + force bool + context *Context } type reader_callback struct { @@ -136,6 +133,11 @@ func (r *Reader) open(options *opts) (*Reader, error) { func (r *Reader) Close() error { var result error + // Free context + if ctx := r.context; ctx != nil { + result = errors.Join(result, ctx.Close()) + } + // Free resources ff.AVFormat_free_context(r.input) if r.avio != nil { @@ -143,6 +145,7 @@ func (r *Reader) Close() error { } // Release resources + r.context = nil r.input = nil r.avio = nil @@ -190,20 +193,20 @@ func (r *Reader) BestStream(t media.Type) int { // Only return if this doesn't have a disposition - so we don't select artwork, for example disposition := r.input.Stream(stream).Disposition() if disposition == 0 || disposition.Is(ff.AV_DISPOSITION_DEFAULT) { - return r.input.Stream(stream).Id() + return r.input.Stream(stream).Index() } } case t.Is(media.AUDIO): if stream, _, err := ff.AVFormat_find_best_stream(r.input, ff.AVMEDIA_TYPE_AUDIO, -1, -1); err == nil { - return r.input.Stream(stream).Id() + return r.input.Stream(stream).Index() } case t.Is(media.SUBTITLE): if stream, _, err := ff.AVFormat_find_best_stream(r.input, ff.AVMEDIA_TYPE_SUBTITLE, -1, -1); err == nil { - return r.input.Stream(stream).Id() + return r.input.Stream(stream).Index() } case t.Is(media.DATA): if stream, _, err := ff.AVFormat_find_best_stream(r.input, ff.AVMEDIA_TYPE_DATA, -1, -1); err == nil { - return r.input.Stream(stream).Id() + return r.input.Stream(stream).Index() } } return -1 @@ -233,24 +236,34 @@ func (r *Reader) Metadata(keys ...string) []*Metadata { return result } -// Decode the media stream into frames. The decodefn is called for each -// frame decoded from the stream. The map function is called for each stream -// and should return the parameters for the destination frame. If the map -// function returns nil, then the stream is ignored. -// -// The decoding can be interrupted by cancelling the context, or by the decodefn -// returning an error or io.EOF. The latter will end the decoding process early but -// will not return an error. func (r *Reader) Decode(ctx context.Context, mapfn DecoderMapFunc, decodefn DecoderFrameFn) error { - // Map streams to decoders - decoders, err := r.mapStreams(mapfn) + // Create a decoding context + decoders, err := newContext(r, mapfn) if err != nil { return err } defer decoders.Close() // Do the decoding - return r.decode(ctx, decoders, decodefn) + return decoders.decode(ctx, decodefn) +} + +// Map streams to decoders, and return the decoding context +// The map function is called for each stream +// and should return the parameters for the destination frame. If any +// parameters are returned as null, then that stream is ignored +func (r *Reader) Map(fn DecoderMapFunc) (*Context, error) { + return newContext(r, fn) +} + +// Decode the media stream into frames. The decodefn is called for each +// frame decoded from the stream. +// +// The decoding can be interrupted by cancelling the context, or by the decodefn +// returning an error or io.EOF. The latter will end the decoding process early but +// will not return an error. +func (r *Reader) DecodeWithContext(ctx context.Context, decoders *Context, decodefn DecoderFrameFn) error { + return decoders.decode(ctx, decodefn) } // Transcode the media stream to a writer @@ -317,120 +330,6 @@ func (r *Reader) Transcode(ctx context.Context, w io.Writer, mapfn DecoderMapFun return result } */ -//////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - DECODE - -type decoderMap map[int]*Decoder - -func (d decoderMap) Close() error { - var result error - for _, decoder := range d { - if err := decoder.Close(); err != nil { - result = errors.Join(result, err) - } - } - return result -} - -// Map streams to decoders, and return the decoders -func (r *Reader) mapStreams(fn DecoderMapFunc) (decoderMap, error) { - decoders := make(decoderMap, r.input.NumStreams()) - - // Standard decoder map function copies all streams - if fn == nil { - fn = func(_ int, par *Par) (*Par, error) { - return par, nil - } - } - - // Create a decoder for each stream - // The decoder map function should be returning the parameters for the - // destination frame. - var result error - for _, stream := range r.input.Streams() { - stream_index := stream.Index() - - // Get decoder parameters and map to a decoder - par, err := fn(stream_index, &Par{ - AVCodecParameters: *stream.CodecPar(), - timebase: stream.TimeBase(), - }) - if err != nil { - result = errors.Join(result, err) - } else if par == nil { - continue - } else if decoder, err := NewDecoder(stream, par, r.force); err != nil { - result = errors.Join(result, err) - } else if _, exists := decoders[stream_index]; exists { - result = errors.Join(result, ErrDuplicateEntry.Withf("stream index %d", stream_index)) - } else { - decoders[stream_index] = decoder - } - } - - // Check to see if we have to do something - if len(decoders) == 0 { - result = errors.Join(result, ErrBadParameter.With("no streams to decode")) - } - - // If there are errors, then free the decoders - if result != nil { - result = errors.Join(result, decoders.Close()) - } - - // Return any errors - return decoders, result -} - -func (r *Reader) decode(ctx context.Context, decoders map[int]*Decoder, fn DecoderFrameFn) error { - // Allocate a packet - packet := ff.AVCodec_packet_alloc() - if packet == nil { - return errors.New("failed to allocate packet") - } - defer ff.AVCodec_packet_free(packet) - - // Read packets -FOR_LOOP: - for { - select { - case <-ctx.Done(): - break FOR_LOOP - default: - if err := ff.AVFormat_read_frame(r.input, packet); errors.Is(err, io.EOF) { - break FOR_LOOP - } else if errors.Is(err, syscall.EAGAIN) { - continue FOR_LOOP - } else if err != nil { - return ErrInternalAppError.With("AVFormat_read_frame: ", err) - } - stream_index := packet.StreamIndex() - if decoder := decoders[stream_index]; decoder != nil { - if err := decoder.decode(packet, fn); errors.Is(err, io.EOF) { - break FOR_LOOP - } else if err != nil { - return err - } - } - } - - // Unreference the packet - ff.AVCodec_packet_unref(packet) - } - - // Flush the decoders - for _, decoder := range decoders { - if err := decoder.decode(nil, fn); errors.Is(err, io.EOF) { - // no-op - } else if err != nil { - return err - } - } - - // Return the context error - will be cancelled, perhaps, or nil if the - // demuxer finished successfully without cancellation - return ctx.Err() -} //////////////////////////////////////////////////////////////////////////////// // PRIVATE METHODS - CALLBACK diff --git a/pkg/ffmpeg/writer.go b/pkg/ffmpeg/writer.go index c59b785..c004348 100644 --- a/pkg/ffmpeg/writer.go +++ b/pkg/ffmpeg/writer.go @@ -230,7 +230,7 @@ func (w *Writer) String() string { // Return a "stream" for encoding func (w *Writer) Stream(stream int) *Encoder { for _, encoder := range w.encoders { - if encoder.stream.Id() == stream { + if encoder.stream.Index() == stream { return encoder } } @@ -255,7 +255,7 @@ func (w *Writer) Encode(ctx context.Context, in EncoderFrameFn, out EncoderPacke // Initialise encoders encoders := make(map[int]*Encoder, len(w.encoders)) for _, encoder := range w.encoders { - stream := encoder.stream.Id() + stream := encoder.stream.Index() if _, exists := encoders[stream]; exists { return ErrBadParameter.Withf("duplicate stream %v", stream) } @@ -334,6 +334,10 @@ func encode(in EncoderFrameFn, out EncoderPacketFn, encoders map[int]*Encoder) e return fmt.Errorf("stream %v: %w", next_stream, err) } } + // If frame not ready, try again + if frame == nil { + return nil + } // Send a frame for encoding if err := next_encoder.Encode(frame, out); err != nil { diff --git a/pkg/sdl/audio.go b/pkg/sdl/audio.go new file mode 100644 index 0000000..c74eb27 --- /dev/null +++ b/pkg/sdl/audio.go @@ -0,0 +1,72 @@ +package sdl + +import ( + "errors" + "fmt" + + // Packages + ffmpeg "github.com/mutablelogic/go-media/pkg/ffmpeg" + sdl "github.com/veandco/go-sdl2/sdl" + + // Namespace imports + . "github.com/djthorpe/go-errors" + . "github.com/mutablelogic/go-media" +) + +////////////////////////////////////////////////////////////////////////////// +// TYPES + +type Audio struct { + device sdl.AudioDeviceID +} + +////////////////////////////////////////////////////////////////////////////// +// GLOBALS + +var ( + mapAudio = map[string]sdl.AudioFormat{ + "u8": sdl.AUDIO_U8, + "s8": sdl.AUDIO_S8, + "s16": sdl.AUDIO_S16SYS, + "flt": sdl.AUDIO_F32SYS, + "fltp": sdl.AUDIO_F32SYS, + } +) + +////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func (s *Context) NewAudio(par *ffmpeg.Par) (*Audio, error) { + if !par.Type().Is(AUDIO) { + return nil, errors.New("invalid audio parameters") + } + + src_format := fmt.Sprint(par.SampleFormat()) + format, exists := mapAudio[src_format] + if !exists { + return nil, ErrBadParameter.Withf("unsupported sample format %q", src_format) + } + + var desired, obtained sdl.AudioSpec + desired.Freq = int32(par.Samplerate()) + desired.Format = format + desired.Channels = uint8(par.ChannelLayout().NumChannels()) + desired.Samples = 1024 + //desired.Callback = s.AudioCallback + + if device, err := sdl.OpenAudioDevice("", false, &desired, &obtained, 0); err != nil { + return nil, err + } else { + return &Audio{device}, nil + } +} + +func (a *Audio) Close() error { + var result error + + // Close the audio device + sdl.CloseAudioDevice(a.device) + + // Return any errors + return result +} diff --git a/pkg/sdl/sdl.go b/pkg/sdl/sdl.go index 249c070..be4e301 100644 --- a/pkg/sdl/sdl.go +++ b/pkg/sdl/sdl.go @@ -1,39 +1,42 @@ -package main +package sdl import ( "context" - "errors" - "log" - "os" - "path/filepath" - "runtime" - "sync" - "time" + "fmt" "unsafe" // Packages - media "github.com/mutablelogic/go-media" - ffmpeg "github.com/mutablelogic/go-media/pkg/ffmpeg" sdl "github.com/veandco/go-sdl2/sdl" + + // Namespace imports + . "github.com/mutablelogic/go-media" ) -type Context struct { -} +/////////////////////////////////////////////////////////////////////////////// +// TYPES -type Window struct { - *sdl.Window - *sdl.Renderer - *sdl.Texture +type Context struct { + evt map[uint32]func(unsafe.Pointer) } -type Surface sdl.Surface +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE // Create a new SDL object which can output audio and video -func NewSDL() (*Context, error) { - if err := sdl.Init(sdl.INIT_VIDEO); err != nil { +func New(t Type) (*Context, error) { + var flags uint32 + if t.Is(VIDEO) { + flags |= sdl.INIT_VIDEO + } + if t.Is(AUDIO) { + flags |= sdl.INIT_AUDIO + } + if err := sdl.Init(flags); err != nil { return nil, err } - return &Context{}, nil + return &Context{ + evt: make(map[uint32]func(unsafe.Pointer)), + }, nil } func (s *Context) Close() error { @@ -41,197 +44,61 @@ func (s *Context) Close() error { return nil } -func (s *Context) NewWindow(title string, width, height int32) (*Window, error) { - window, err := sdl.CreateWindow( - title, - sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED, - width, height, - sdl.WINDOW_SHOWN|sdl.WINDOW_BORDERLESS) - if err != nil { - return nil, err - } - renderer, err := sdl.CreateRenderer(window, -1, sdl.RENDERER_ACCELERATED) - if err != nil { - window.Destroy() - return nil, err - } - texture, err := renderer.CreateTexture(sdl.PIXELFORMAT_IYUV, sdl.TEXTUREACCESS_STREAMING, width, height) - if err != nil { - renderer.Destroy() - window.Destroy() - return nil, err - } - return &Window{window, renderer, texture}, nil -} +/////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS -func (w *Window) Close() error { - var result error - if err := (*sdl.Texture)(w.Texture).Destroy(); err != nil { - result = errors.Join(result, err) - } - if err := (*sdl.Renderer)(w.Renderer).Destroy(); err != nil { - result = errors.Join(result, err) - } - if err := (*sdl.Window)(w.Window).Destroy(); err != nil { - result = errors.Join(result, err) - } - w.Texture = nil - w.Renderer = nil - w.Window = nil - - // Return any errors - return result -} - -func (w *Window) Flush() error { - if err := w.Renderer.Copy(w.Texture, nil, nil); err != nil { - return err - } - w.Renderer.Present() - return nil -} - -func (w *Window) RenderFrame(frame *ffmpeg.Frame) error { - return w.UpdateYUV( - nil, - frame.Bytes(0), - frame.Stride(0), - frame.Bytes(1), - frame.Stride(1), - frame.Bytes(2), - frame.Stride(2), - ) +func (ctx *Context) Register(fn func(userInfo unsafe.Pointer)) uint32 { + evt := sdl.RegisterEvents(1) + ctx.evt[evt] = fn + return evt } -func (s *Context) RunLoop(w *Window, evt uint32) { - runtime.LockOSThread() - running := true - - pts := ffmpeg.TS_UNDEFINED - for running { - for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { - switch event := event.(type) { - case *sdl.QuitEvent: - running = false - case *sdl.UserEvent: - if event.Type != evt { - break - } - - // Get the video frame - if nil, then end of stream - frame := (*ffmpeg.Frame)(event.Data1) - if frame == nil { - running = false - break - } - - // Pause to present the frame at the correct PTS - if pts != ffmpeg.TS_UNDEFINED && pts < frame.Ts() { - pause := frame.Ts() - pts - if pause > 0 { - sdl.Delay(uint32(pause * 1000)) - } - } - - // Set current timestamp - pts = frame.Ts() - - // Render the frame, release the frame resources - if err := w.RenderFrame(frame); err != nil { - log.Print(err) - } else if err := w.Flush(); err != nil { - log.Print(err) - } else if err := frame.Close(); err != nil { - log.Print(err) - } - - } - } - } +func (ctx *Context) Post(evt uint32, userInfo unsafe.Pointer) { + sdl.PushEvent(&sdl.UserEvent{ + Type: evt, + Data1: userInfo, + }) } -func main() { - ctx, err := NewSDL() - if err != nil { - log.Fatal(err) - } - defer ctx.Close() - - // Register an event for a new frame - evt := sdl.RegisterEvents(1) - - // Open video - input, err := ffmpeg.Open(os.Args[1]) - if err != nil { - log.Fatal(err) - } - - // Decode frames in a goroutine - var result error - var wg sync.WaitGroup - var w, h int32 - - // Decoder map function - mapfn := func(stream int, par *ffmpeg.Par) (*ffmpeg.Par, error) { - if stream == input.BestStream(media.VIDEO) { - w = int32(par.Width()) - h = int32(par.Height()) - return par, nil - } - return nil, nil - } - - wg.Add(1) +func (ctx *Context) Run(parent context.Context) error { + // Register an event which quits the application when context is cancelled + evtCancel := sdl.RegisterEvents(1) go func() { - defer wg.Done() - err := input.Decode(context.Background(), mapfn, func(stream int, frame *ffmpeg.Frame) error { - copy, err := frame.Copy() - if err != nil { - copy.Close() - return err - } - sdl.PushEvent(&sdl.UserEvent{ - Type: evt, - Data1: unsafe.Pointer(copy), - }) - return nil - }) - if err != nil { - result = errors.Join(result, err) - } - // Quit event - sdl.PushEvent(&sdl.QuitEvent{ - Type: sdl.QUIT, + <-parent.Done() + sdl.PushEvent(&sdl.UserEvent{ + Type: evtCancel, }) }() - // HACK - time.Sleep(100 * time.Millisecond) - if w == 0 || h == 0 { - log.Fatal("No video stream found") - } + // Start the runloop + quit := false + for { + if quit { + break + } - title := filepath.Base(os.Args[1]) - meta := input.Metadata("title") - if len(meta) > 0 { - title = meta[0].Value() - } + // Wait on an event + evt := sdl.WaitEvent() - // Create a new window - window, err := ctx.NewWindow(title, w, h) - if err != nil { - log.Fatal(err) + // Handle cancel, custom, keyboard and quit events + switch evt := evt.(type) { + case *sdl.UserEvent: + if evt.GetType() == evtCancel { + quit = true + } else if fn, exists := ctx.evt[evt.GetType()]; exists { + fn(evt.Data1) + } + case *sdl.QuitEvent: + quit = true + case *sdl.KeyboardEvent: + if evt.GetType() == sdl.KEYDOWN { + quit = true + } + default: + fmt.Println("Unhandled event:", evt.GetType()) + } } - defer window.Close() - - // Run the SDL loop until quit - ctx.RunLoop(window, evt) - // Wait until all goroutines have finished - wg.Wait() - - // Return any errors - if result != nil { - log.Fatal(result) - } + // TODO: Return any errors + return nil } diff --git a/pkg/sdl/sdl.go_old b/pkg/sdl/sdl.go_old new file mode 100644 index 0000000..249c070 --- /dev/null +++ b/pkg/sdl/sdl.go_old @@ -0,0 +1,237 @@ +package main + +import ( + "context" + "errors" + "log" + "os" + "path/filepath" + "runtime" + "sync" + "time" + "unsafe" + + // Packages + media "github.com/mutablelogic/go-media" + ffmpeg "github.com/mutablelogic/go-media/pkg/ffmpeg" + sdl "github.com/veandco/go-sdl2/sdl" +) + +type Context struct { +} + +type Window struct { + *sdl.Window + *sdl.Renderer + *sdl.Texture +} + +type Surface sdl.Surface + +// Create a new SDL object which can output audio and video +func NewSDL() (*Context, error) { + if err := sdl.Init(sdl.INIT_VIDEO); err != nil { + return nil, err + } + return &Context{}, nil +} + +func (s *Context) Close() error { + sdl.Quit() + return nil +} + +func (s *Context) NewWindow(title string, width, height int32) (*Window, error) { + window, err := sdl.CreateWindow( + title, + sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED, + width, height, + sdl.WINDOW_SHOWN|sdl.WINDOW_BORDERLESS) + if err != nil { + return nil, err + } + renderer, err := sdl.CreateRenderer(window, -1, sdl.RENDERER_ACCELERATED) + if err != nil { + window.Destroy() + return nil, err + } + texture, err := renderer.CreateTexture(sdl.PIXELFORMAT_IYUV, sdl.TEXTUREACCESS_STREAMING, width, height) + if err != nil { + renderer.Destroy() + window.Destroy() + return nil, err + } + return &Window{window, renderer, texture}, nil +} + +func (w *Window) Close() error { + var result error + if err := (*sdl.Texture)(w.Texture).Destroy(); err != nil { + result = errors.Join(result, err) + } + if err := (*sdl.Renderer)(w.Renderer).Destroy(); err != nil { + result = errors.Join(result, err) + } + if err := (*sdl.Window)(w.Window).Destroy(); err != nil { + result = errors.Join(result, err) + } + w.Texture = nil + w.Renderer = nil + w.Window = nil + + // Return any errors + return result +} + +func (w *Window) Flush() error { + if err := w.Renderer.Copy(w.Texture, nil, nil); err != nil { + return err + } + w.Renderer.Present() + return nil +} + +func (w *Window) RenderFrame(frame *ffmpeg.Frame) error { + return w.UpdateYUV( + nil, + frame.Bytes(0), + frame.Stride(0), + frame.Bytes(1), + frame.Stride(1), + frame.Bytes(2), + frame.Stride(2), + ) +} + +func (s *Context) RunLoop(w *Window, evt uint32) { + runtime.LockOSThread() + running := true + + pts := ffmpeg.TS_UNDEFINED + for running { + for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { + switch event := event.(type) { + case *sdl.QuitEvent: + running = false + case *sdl.UserEvent: + if event.Type != evt { + break + } + + // Get the video frame - if nil, then end of stream + frame := (*ffmpeg.Frame)(event.Data1) + if frame == nil { + running = false + break + } + + // Pause to present the frame at the correct PTS + if pts != ffmpeg.TS_UNDEFINED && pts < frame.Ts() { + pause := frame.Ts() - pts + if pause > 0 { + sdl.Delay(uint32(pause * 1000)) + } + } + + // Set current timestamp + pts = frame.Ts() + + // Render the frame, release the frame resources + if err := w.RenderFrame(frame); err != nil { + log.Print(err) + } else if err := w.Flush(); err != nil { + log.Print(err) + } else if err := frame.Close(); err != nil { + log.Print(err) + } + + } + } + } +} + +func main() { + ctx, err := NewSDL() + if err != nil { + log.Fatal(err) + } + defer ctx.Close() + + // Register an event for a new frame + evt := sdl.RegisterEvents(1) + + // Open video + input, err := ffmpeg.Open(os.Args[1]) + if err != nil { + log.Fatal(err) + } + + // Decode frames in a goroutine + var result error + var wg sync.WaitGroup + var w, h int32 + + // Decoder map function + mapfn := func(stream int, par *ffmpeg.Par) (*ffmpeg.Par, error) { + if stream == input.BestStream(media.VIDEO) { + w = int32(par.Width()) + h = int32(par.Height()) + return par, nil + } + return nil, nil + } + + wg.Add(1) + go func() { + defer wg.Done() + err := input.Decode(context.Background(), mapfn, func(stream int, frame *ffmpeg.Frame) error { + copy, err := frame.Copy() + if err != nil { + copy.Close() + return err + } + sdl.PushEvent(&sdl.UserEvent{ + Type: evt, + Data1: unsafe.Pointer(copy), + }) + return nil + }) + if err != nil { + result = errors.Join(result, err) + } + // Quit event + sdl.PushEvent(&sdl.QuitEvent{ + Type: sdl.QUIT, + }) + }() + + // HACK + time.Sleep(100 * time.Millisecond) + if w == 0 || h == 0 { + log.Fatal("No video stream found") + } + + title := filepath.Base(os.Args[1]) + meta := input.Metadata("title") + if len(meta) > 0 { + title = meta[0].Value() + } + + // Create a new window + window, err := ctx.NewWindow(title, w, h) + if err != nil { + log.Fatal(err) + } + defer window.Close() + + // Run the SDL loop until quit + ctx.RunLoop(window, evt) + + // Wait until all goroutines have finished + wg.Wait() + + // Return any errors + if result != nil { + log.Fatal(result) + } +} diff --git a/pkg/sdl/video.go b/pkg/sdl/video.go new file mode 100644 index 0000000..057a718 --- /dev/null +++ b/pkg/sdl/video.go @@ -0,0 +1,97 @@ +package sdl + +import ( + "errors" + + // Packages + ffmpeg "github.com/mutablelogic/go-media/pkg/ffmpeg" + sdl "github.com/veandco/go-sdl2/sdl" + + // Namespace imports + . "github.com/mutablelogic/go-media" +) + +////////////////////////////////////////////////////////////////////////////// +// TYPES + +type Window struct { + *sdl.Window + *sdl.Renderer + *sdl.Texture +} + +////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +func (s *Context) NewVideo(title string, par *ffmpeg.Par) (*Window, error) { + if !par.Type().Is(VIDEO) || par.Width() <= 0 || par.Height() <= 0 { + return nil, errors.New("invalid video parameters") + } + window, err := sdl.CreateWindow( + title, + sdl.WINDOWPOS_CENTERED, sdl.WINDOWPOS_CENTERED, + int32(par.Width()), int32(par.Height()), + sdl.WINDOW_SHOWN|sdl.WINDOW_BORDERLESS) + if err != nil { + return nil, err + } + renderer, err := sdl.CreateRenderer(window, -1, sdl.RENDERER_ACCELERATED) + if err != nil { + window.Destroy() + return nil, err + } + texture, err := renderer.CreateTexture(sdl.PIXELFORMAT_IYUV, sdl.TEXTUREACCESS_STREAMING, int32(par.Width()), int32(par.Height())) + if err != nil { + renderer.Destroy() + window.Destroy() + return nil, err + } + return &Window{window, renderer, texture}, nil +} + +func (w *Window) Close() error { + var result error + if err := (*sdl.Texture)(w.Texture).Destroy(); err != nil { + result = errors.Join(result, err) + } + if err := (*sdl.Renderer)(w.Renderer).Destroy(); err != nil { + result = errors.Join(result, err) + } + if err := (*sdl.Window)(w.Window).Destroy(); err != nil { + result = errors.Join(result, err) + } + w.Texture = nil + w.Renderer = nil + w.Window = nil + + // Return any errors + return result +} + +////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS + +func (w *Window) Flush() error { + if err := w.Renderer.Copy(w.Texture, nil, nil); err != nil { + return err + } + w.Renderer.Present() + return nil +} + +func (w *Window) RenderFrame(frame *ffmpeg.Frame) error { + if err := w.UpdateYUV( + nil, + frame.Bytes(0), + frame.Stride(0), + frame.Bytes(1), + frame.Stride(1), + frame.Bytes(2), + frame.Stride(2), + ); err != nil { + return err + } + + // Return success + return nil +}