diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e043a20 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +examples/bin/ diff --git a/examples/build.sh b/examples/build.sh index a7f2d9d..c705d04 100755 --- a/examples/build.sh +++ b/examples/build.sh @@ -12,3 +12,4 @@ go build -o bin/simple-transcode ./simple-transcode echo "" echo "Build complete!" echo "Run with: ./bin/simple-transcode " +echo "Example: ./bin/simple-transcode test.mp4 output.mp4" diff --git a/examples/simple-transcode/main.go b/examples/simple-transcode/main.go index 65ad3fe..4727367 100644 --- a/examples/simple-transcode/main.go +++ b/examples/simple-transcode/main.go @@ -8,7 +8,7 @@ import ( "git.kingecg.top/kingecg/goffmpeg/pkg/ffmpeg" ) -// Simple transcoding example using goffmpeg library +// Simple remuxing example using goffmpeg library func main() { if len(os.Args) < 3 { fmt.Println("Usage: simple-transcode ") @@ -26,50 +26,149 @@ func main() { if err := ic.OpenInput(inputURL); err != nil { log.Fatalf("Failed to open input %s: %v", inputURL, err) } - defer ic.Close() // Find stream info if err := ic.FindStreamInfo(); err != nil { log.Fatalf("Failed to find stream info: %v", err) } - // Dump input format info fmt.Printf("Input: %s\n", inputURL) ic.DumpFormat(0, inputURL, false) - // Find video stream - videoStreams := ic.VideoStreams() - if len(videoStreams) == 0 { - log.Fatal("No video stream found in input") - } - - vs := videoStreams[0] - fmt.Printf("Video stream index: %d\n", vs.Index()) - - // Get codec parameters - cp := vs.CodecParameters() - fmt.Printf("Codec type: %d, Codec ID: %d\n", cp.CodecType(), cp.CodecID()) - - // Create output context + // Guess output format of := ffmpeg.GuessFormat("", outputURL) if of == nil { log.Fatalf("Failed to guess output format") } + // Create output context ofc, err := ffmpeg.AllocOutputContext(outputURL, of) if err != nil { log.Fatalf("Failed to allocate output context: %v", err) } defer ofc.Free() - // Copy stream from input to output - _, err = ofc.AddStream(nil) - if err != nil { - log.Fatalf("Failed to add stream: %v", err) + // Get input streams + inputStreams := ic.Streams() + + // Process streams - select first video and audio only + var videoIdx, audioIdx int = -1, -1 + streamCount := 0 + + for i, is := range inputStreams { + cp := is.CodecParameters() + if cp == nil { + continue + } + + streamType := cp.CodecType() + + // Skip non-video/audio + if streamType != ffmpeg.CodecTypeVideo && streamType != ffmpeg.CodecTypeAudio { + continue + } + + // Skip duplicate audio + if streamType == ffmpeg.CodecTypeAudio && audioIdx >= 0 { + fmt.Printf("\nStream %d: audio (skipped - duplicate)\n", i) + continue + } + + // Add stream with same codec (stream copy mode) + os, err := ofc.AddStream(nil) + if err != nil { + fmt.Printf("\nStream %d: failed to add stream: %v\n", i, err) + continue + } + + // Copy codec parameters from input to output + if err := os.SetCodecParameters(cp); err != nil { + fmt.Printf("\nStream %d: failed to set codec parameters: %v\n", i, err) + continue + } + + // Copy time base + tb := is.TimeBase() + os.SetTimeBase(tb) + + if streamType == ffmpeg.CodecTypeVideo { + videoIdx = streamCount + fmt.Printf("\nStream %d: video (stream copy)\n", i) + } else { + audioIdx = streamCount + fmt.Printf("\nStream %d: audio (stream copy)\n", i) + } + streamCount++ + } + + if videoIdx < 0 && audioIdx < 0 { + log.Fatal("No supported streams found") } fmt.Printf("\nOutput: %s\n", outputURL) - fmt.Println("Transcoding setup complete. Use the library APIs to process frames.") + fmt.Printf("Output has %d streams\n", streamCount) + ofc.DumpFormat(0, outputURL, true) - _ = vs // vs is used for reference + // Open output file + if err := ofc.OpenOutput(outputURL); err != nil { + log.Fatalf("Failed to open output: %v", err) + } + + // Write header + if err := ofc.WriteHeader(); err != nil { + log.Fatalf("Failed to write header: %v", err) + } + + fmt.Println("\nRemuxing...") + pkt := ffmpeg.AllocPacket() + defer pkt.Free() + + packetCount := 0 + for { + err := ic.ReadPacket(pkt) + if err != nil { + break + } + + pktStreamIdx := pkt.StreamIndex() + + // Map input stream index to output stream index + var outStreamIdx int + if videoIdx >= 0 && audioIdx >= 0 { + // Both video and audio present + if pktStreamIdx == 0 { + outStreamIdx = videoIdx + } else if pktStreamIdx == 1 { + outStreamIdx = audioIdx + } else { + pkt.Unref() + continue + } + } else if videoIdx >= 0 { + outStreamIdx = videoIdx + } else { + outStreamIdx = audioIdx + } + + pkt.SetStreamIndex(outStreamIdx) + + if err := ofc.WritePacket(pkt); err != nil { + log.Printf("Warning: failed to write packet: %v", err) + } + + pkt.Unref() + packetCount++ + + if packetCount%100 == 0 { + fmt.Printf("Processed %d packets...\n", packetCount) + } + } + + // Write trailer + if err := ofc.WriteTrailer(); err != nil { + log.Fatalf("Failed to write trailer: %v", err) + } + + fmt.Printf("\nRemuxing complete! Processed %d packets.\n", packetCount) + fmt.Printf("Output saved to: %s\n", outputURL) } diff --git a/examples/simple-transcode/simple-transcode b/examples/simple-transcode/simple-transcode new file mode 100755 index 0000000..94a6a9b Binary files /dev/null and b/examples/simple-transcode/simple-transcode differ diff --git a/pkg/ffmpeg/codec.go b/pkg/ffmpeg/codec.go index 2ebb5dc..bedf3a0 100644 --- a/pkg/ffmpeg/codec.go +++ b/pkg/ffmpeg/codec.go @@ -195,6 +195,47 @@ func (c *Context) SetBitRate(br int64) { } } +// TimeBase returns the time base +func (c *Context) TimeBase() Rational { + if c.ptr == nil { + return Rational{} + } + return Rational{ + num: int(c.ptr.time_base.num), + den: int(c.ptr.time_base.den), + } +} + +// SetTimeBase sets the time base +func (c *Context) SetTimeBase(r Rational) { + if c.ptr != nil { + c.ptr.time_base.num = C.int(r.num) + c.ptr.time_base.den = C.int(r.den) + } +} + +// SetChannelLayout sets the channel layout +func (c *Context) SetChannelLayout(layout uint64) { + if c.ptr != nil { + c.ptr.channel_layout = C.uint64_t(layout) + } +} + +// SetSampleRate sets the sample rate +func (c *Context) SetSampleRate(rate int) { + if c.ptr != nil { + c.ptr.sample_rate = C.int(rate) + } +} + +// Channels returns the number of channels +func (c *Context) Channels() int { + if c.ptr == nil { + return 0 + } + return int(c.ptr.channels) +} + // Open opens the codec func (c *Context) Open(codec *Codec) error { if c.ptr == nil || codec == nil || codec.ptr == nil { @@ -212,6 +253,23 @@ func (c *Context) Open(codec *Codec) error { return nil } +// CopyParameters copies parameters from CodecParameters to this context +func (c *Context) CopyParameters(cp *CodecParameters) error { + if c.ptr == nil || cp == nil || cp.ptr == nil { + return ErrInvalidCodec + } + + ret := C.avcodec_parameters_to_context(c.ptr, cp.ptr) + if ret < 0 { + return &FFmpegError{ + Code: int(ret), + Message: "failed to copy parameters", + Op: "CopyParameters", + } + } + return nil +} + // SendPacket sends a packet to the decoder func (c *Context) SendPacket(pkt *Packet) error { if c.ptr == nil { diff --git a/pkg/ffmpeg/ffmpeg.go b/pkg/ffmpeg/ffmpeg.go index adb79d1..1ce8131 100644 --- a/pkg/ffmpeg/ffmpeg.go +++ b/pkg/ffmpeg/ffmpeg.go @@ -238,6 +238,33 @@ func (s *Stream) SetCodecParameters(cp *CodecParameters) error { return nil } +// SetCodecContextParameters copies parameters from a codec context to this stream +func (s *Stream) SetCodecContextParameters(cc *Context) error { + if s.ptr == nil || cc == nil || cc.ptr == nil { + return ErrInvalidCodec + } + + // Manually copy fields from codec context to codec parameters + s.ptr.codecpar.codec_type = cc.ptr.codec_type + s.ptr.codecpar.codec_id = cc.ptr.codec_id + s.ptr.codecpar.bit_rate = cc.ptr.bit_rate + s.ptr.codecpar.width = cc.ptr.width + s.ptr.codecpar.height = cc.ptr.height + + // Copy format based on codec type + if cc.ptr.codec_type == C.AVMEDIA_TYPE_VIDEO { + s.ptr.codecpar.format = C.int(cc.ptr.pix_fmt) + } else if cc.ptr.codec_type == C.AVMEDIA_TYPE_AUDIO { + s.ptr.codecpar.format = C.int(cc.ptr.sample_fmt) + } + + s.ptr.codecpar.sample_rate = cc.ptr.sample_rate + s.ptr.codecpar.channels = cc.ptr.channels + s.ptr.codecpar.channel_layout = cc.ptr.channel_layout + + return nil +} + // Codec returns the codec context (deprecated: use CodecParameters instead) func (s *Stream) Codec() *Context { // In FFmpeg 4.0+, codec field was removed from AVStream @@ -401,13 +428,18 @@ func AllocOutputContext(url string, fmt *OutputFormat) (*OutputFormatContext, er return ofc, nil } -// AddStream adds a new stream +// AddStream adds a new stream with optional codec func (ofc *OutputFormatContext) AddStream(codec *Codec) (*Stream, error) { - if ofc.ptr == nil || codec == nil || codec.ptr == nil { - return nil, ErrInvalidCodec + if ofc.ptr == nil { + return nil, ErrInvalidOutput } - stream := C.avformat_new_stream(ofc.ptr, codec.ptr) + var stream *C.AVStream + if codec != nil && codec.ptr != nil { + stream = C.avformat_new_stream(ofc.ptr, codec.ptr) + } else { + stream = C.avformat_new_stream(ofc.ptr, nil) + } if stream == nil { return nil, ErrInvalidCodec } @@ -422,6 +454,33 @@ func (ofc *OutputFormatContext) SetOformat(fmt *OutputFormat) { } } +// OpenOutput opens the output file +func (ofc *OutputFormatContext) OpenOutput(url string) error { + if ofc.ptr == nil { + return ErrInvalidOutput + } + + cURL := C.CString(url) + defer C.free(unsafe.Pointer(cURL)) + + ret := C.avio_open(&ofc.ptr.pb, cURL, C.AVIO_FLAG_WRITE) + if ret < 0 { + return &FFmpegError{ + Code: int(ret), + Message: "failed to open output", + Op: "OpenOutput", + } + } + return nil +} + +// CloseOutput closes the output file +func (ofc *OutputFormatContext) CloseOutput() { + if ofc.ptr != nil && ofc.ptr.pb != nil { + C.avio_close(ofc.ptr.pb) + } +} + // DumpFormat dumps format info func (ofc *OutputFormatContext) DumpFormat(idx int, url string, isOutput bool) { ofc.FormatContext.DumpFormat(idx, url, isOutput) @@ -433,12 +492,6 @@ func (ofc *OutputFormatContext) WriteHeader() error { return ErrInvalidOutput } - if (unsafe.Pointer(ofc.ptr.pb) != nil) && (ofc.ptr.flags&C.AVFMT_NOFILE) == 0 { - // file handle has been created by the caller - } else { - // let avformat do it - } - ret := C.avformat_write_header(ofc.ptr, nil) if ret < 0 { return &FFmpegError{