diff --git a/README.md b/README.md index 7dd5d9a..3321da3 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Examples are located in the [examples](examples) directory and mirror as much as |---|---|---| |BitStream Filtering|[see](examples/bit_stream_filtering/main.go)|X |Custom IO Demuxing|[see](examples/custom_io_demuxing/main.go)|[see](https://github.com/FFmpeg/FFmpeg/blob/n7.0/doc/examples/avio_reading.c) +|Custom IO Muxing|[see](examples/custom_io_muxing/main.go)|X |Demuxing/Decoding|[see](examples/demuxing_decoding/main.go)|[see](https://github.com/FFmpeg/FFmpeg/blob/n7.0/doc/examples/demuxing_decoding.c) |Filtering|[see](examples/filtering/main.go)|[see](https://github.com/FFmpeg/FFmpeg/blob/n7.0/doc/examples/filtering_video.c) |Frame data manipulation|[see](examples/frame_data_manipulation/main.go)|X diff --git a/examples/custom_io_muxing/main.go b/examples/custom_io_muxing/main.go new file mode 100644 index 0000000..4f2dce3 --- /dev/null +++ b/examples/custom_io_muxing/main.go @@ -0,0 +1,185 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "log" + "os" + "strings" + + "github.com/asticode/go-astiav" +) + +var ( + input = flag.String("i", "", "the input path") + output = flag.String("o", "", "the output path") +) + +func main() { + // Handle ffmpeg logs + astiav.SetLogLevel(astiav.LogLevelDebug) + astiav.SetLogCallback(func(c astiav.Classer, l astiav.LogLevel, fmt, msg string) { + var cs string + if c != nil { + if cl := c.Class(); cl != nil { + cs = " - class: " + cl.String() + } + } + log.Printf("ffmpeg log: %s%s - level: %d\n", strings.TrimSpace(msg), cs, l) + }) + + // Parse flags + flag.Parse() + + // Usage + if *input == "" || *output == "" { + log.Println("Usage: -i -o ") + return + } + + // Allocate packet + pkt := astiav.AllocPacket() + defer pkt.Free() + + // Allocate input format context + inputFormatContext := astiav.AllocFormatContext() + if inputFormatContext == nil { + log.Fatal(errors.New("main: input format context is nil")) + } + defer inputFormatContext.Free() + + // Open input + if err := inputFormatContext.OpenInput(*input, nil, nil); err != nil { + log.Fatal(fmt.Errorf("main: opening input failed: %w", err)) + } + defer inputFormatContext.CloseInput() + + // Find stream info + if err := inputFormatContext.FindStreamInfo(nil); err != nil { + log.Fatal(fmt.Errorf("main: finding stream info failed: %w", err)) + } + + // Allocate output format context + outputFormatContext, err := astiav.AllocOutputFormatContext(nil, "mp4", "") + if err != nil { + log.Fatal(fmt.Errorf("main: allocating output format context failed: %w", err)) + } + if outputFormatContext == nil { + log.Fatal(errors.New("main: output format context is nil")) + } + defer outputFormatContext.Free() + + // Open file + f, err := os.Create(*output) + if err != nil { + log.Fatal(fmt.Errorf("main: creating %s failed: %w", *output, err)) + } + defer f.Close() + + // Allocate io context + ioContext, err := astiav.AllocIOContext( + 4096, + true, + nil, + func(offset int64, whence int) (n int64, err error) { + return f.Seek(offset, whence) + }, + func(b []byte) (n int, err error) { + return f.Write(b) + }, + ) + if err != nil { + log.Fatal(fmt.Errorf("main: allocating io context failed: %w", err)) + } + defer ioContext.Free() + + // Store io context + outputFormatContext.SetPb(ioContext) + + // Loop through streams + inputStreams := make(map[int]*astiav.Stream) // Indexed by input stream index + outputStreams := make(map[int]*astiav.Stream) // Indexed by input stream index + for _, is := range inputFormatContext.Streams() { + // Only process audio or video + if is.CodecParameters().MediaType() != astiav.MediaTypeAudio && + is.CodecParameters().MediaType() != astiav.MediaTypeVideo { + continue + } + + // Add input stream + inputStreams[is.Index()] = is + + // Add stream to output format context + os := outputFormatContext.NewStream(nil) + if os == nil { + log.Fatal(errors.New("main: output stream is nil")) + } + + // Copy codec parameters + if err = is.CodecParameters().Copy(os.CodecParameters()); err != nil { + log.Fatal(fmt.Errorf("main: copying codec parameters failed: %w", err)) + } + + // Reset codec tag + os.CodecParameters().SetCodecTag(0) + + // Add output stream + outputStreams[is.Index()] = os + } + + // Write header + if err = outputFormatContext.WriteHeader(nil); err != nil { + log.Fatal(fmt.Errorf("main: writing header failed: %w", err)) + } + + // Loop through packets + for { + // We use a closure to ease unreferencing packet + if stop := func() bool { + // Read frame + if err = inputFormatContext.ReadFrame(pkt); err != nil { + if errors.Is(err, astiav.ErrEof) { + return true + } + log.Fatal(fmt.Errorf("main: reading frame failed: %w", err)) + } + + // Make sure to unreference packet + defer pkt.Unref() + + // Get input stream + inputStream, ok := inputStreams[pkt.StreamIndex()] + if !ok { + return false + } + + // Get output stream + outputStream, ok := outputStreams[pkt.StreamIndex()] + if !ok { + return false + } + + // Update packet + pkt.SetStreamIndex(outputStream.Index()) + pkt.RescaleTs(inputStream.TimeBase(), outputStream.TimeBase()) + pkt.SetPos(-1) + + // Write frame + if err = outputFormatContext.WriteInterleavedFrame(pkt); err != nil { + log.Fatal(fmt.Errorf("main: writing interleaved frame failed: %w", err)) + } + return false + }(); stop { + break + } + } + + // Write trailer + if err = outputFormatContext.WriteTrailer(); err != nil { + log.Fatal(fmt.Errorf("main: writing trailer failed: %w", err)) + } + + // Success + log.Println("success") +}