mirror of
https://github.com/qrtc/ffmpeg-dev-go.git
synced 2025-10-04 07:17:36 +08:00
352 lines
11 KiB
Go
352 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
ffmpeg "github.com/qrtc/ffmpeg-dev-go"
|
|
)
|
|
|
|
var (
|
|
audioStream *ffmpeg.AVStream
|
|
videoStream *ffmpeg.AVStream
|
|
videoFrameCount int
|
|
audioFrameCount int
|
|
width, height int32
|
|
pixFmt ffmpeg.AVPixelFormat
|
|
audioDstFile *os.File
|
|
videoDstFile *os.File
|
|
audioStreamIdx int32
|
|
videoStreamIdx int32
|
|
videoDstData [4]*uint8
|
|
videoDstLinesize [4]int32
|
|
videoDstBufSize int32
|
|
srcFilename string
|
|
)
|
|
|
|
func outputVideoFrame(videoDecCtx *ffmpeg.AVCodecContext, frame *ffmpeg.AVFrame) int32 {
|
|
|
|
if frame.GetWidth() != width || frame.GetHeight() != height || frame.GetFormat() != pixFmt {
|
|
// To handle this change, one could call av_image_alloc again and
|
|
// decode the following frames into another rawvideo file.
|
|
fmt.Fprintf(os.Stderr, "Error: Width, height and pixel format have to be "+
|
|
"constant in a rawvideo file, but the width, height or "+
|
|
"pixel format of the input video changed:\n"+
|
|
"old: width = %d, height = %d, format = %s\n"+
|
|
"new: width = %d, height = %d, format = %s\n",
|
|
width, height, ffmpeg.AvGetPixFmtName(pixFmt),
|
|
frame.GetWidth(), frame.GetHeight(),
|
|
ffmpeg.AvGetPixFmtName(frame.GetFormat()))
|
|
return -1
|
|
}
|
|
|
|
videoFrameCount++
|
|
fmt.Fprintf(os.Stdout, "video_frame n:%d coded_n:%d\n", videoFrameCount, frame.GetCodedPictureNumber())
|
|
|
|
// copy decoded frame to destination buffer:
|
|
// this is required since rawvideo expects non aligned data
|
|
ffmpeg.AvImageCopy(videoDstData[:], videoDstLinesize[:], frame.GetData(), frame.GetLinesize(), pixFmt, width, height)
|
|
|
|
// write to rawvideo file
|
|
videoDstFile.Write(unsafe.Slice(videoDstData[0], videoDstBufSize))
|
|
return 0
|
|
}
|
|
|
|
func outputAudioFrame(audioDecCtx *ffmpeg.AVCodecContext, frame *ffmpeg.AVFrame) (ret int32) {
|
|
unpaddedLinesize := frame.GetNbSamples() * ffmpeg.AvGetBytesPerSample(frame.GetFormat())
|
|
audioFrameCount++
|
|
fmt.Fprintf(os.Stdout, "audio_frame n:%d nb_samples:%d pts:%s\n",
|
|
audioFrameCount, frame.GetNbSamples(),
|
|
ffmpeg.AvTs2timestr(frame.GetPts(), audioDecCtx.GetPktTimebaseAddr()))
|
|
|
|
// Write the raw audio data samples of the first plane. This works
|
|
// fine for packed formats (e.g. AV_SAMPLE_FMT_S16). However,
|
|
// most audio decoders output planar audio, which uses a separate
|
|
// plane of audio samples for each channel (e.g. AV_SAMPLE_FMT_S16P).
|
|
// In other words, this code will write only the first audio channel
|
|
// in these cases.
|
|
// You should use libswresample or libavfilter to convert the frame
|
|
// to packed data.
|
|
audioDstFile.Write(ffmpeg.SliceSlice(frame.GetExtendedData(), 1, unpaddedLinesize)[0])
|
|
|
|
return 0
|
|
}
|
|
|
|
func deocdePacket(dec *ffmpeg.AVCodecContext, pkt *ffmpeg.AVPacket, frame *ffmpeg.AVFrame) (ret int32) {
|
|
|
|
// submit the packet to the decoder
|
|
if ret = ffmpeg.AvCodecSendPacket(dec, pkt); ret < 0 {
|
|
fmt.Fprintf(os.Stderr, "Error submitting a packet for decoding (%s)\n", ffmpeg.AvErr2str(ret))
|
|
return ret
|
|
}
|
|
|
|
// get all the available frames from the decoder
|
|
for ret >= 0 {
|
|
if ret = ffmpeg.AvCodecReceiveFrame(dec, frame); ret < 0 {
|
|
// those two return values are special and mean there is no output
|
|
// frame available, but there were no errors during decoding
|
|
if ret == ffmpeg.AVERROR_EOF || ret == ffmpeg.AVERROR(syscall.EAGAIN) {
|
|
return 0
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "Error during decoding (%s)\n", ffmpeg.AvErr2str(ret))
|
|
return ret
|
|
}
|
|
|
|
// write the frame data to output file
|
|
if dec.GetCodec().GetType() == ffmpeg.AVMEDIA_TYPE_VIDEO {
|
|
ret = outputVideoFrame(dec, frame)
|
|
} else {
|
|
ret = outputAudioFrame(dec, frame)
|
|
}
|
|
|
|
ffmpeg.AvFrameUnref(frame)
|
|
if ret < 0 {
|
|
return ret
|
|
}
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
func openCodecContext(fmtCtx *ffmpeg.AVFormatContext, _type ffmpeg.AVMediaType) (
|
|
streamIdx int32, decCtx *ffmpeg.AVCodecContext, ret int32) {
|
|
var (
|
|
opts *ffmpeg.AVDictionary
|
|
)
|
|
if ret = ffmpeg.AvFindBestStream(fmtCtx, _type, -1, -1, nil, 0); ret < 0 {
|
|
fmt.Fprintf(os.Stderr, "Could not find %s stream in input file '%s'\n",
|
|
ffmpeg.AvGetMediaTypeString(_type), srcFilename)
|
|
return 0, nil, ret
|
|
} else {
|
|
streamIdx = ret
|
|
st := fmtCtx.GetStreams()[streamIdx]
|
|
|
|
// find decoder for the stream
|
|
dec := ffmpeg.AvCodecFindDecoder(st.GetCodecpar().GetCodecId())
|
|
if dec == nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to find %s codec\n",
|
|
ffmpeg.AvGetMediaTypeString(_type))
|
|
return 0, nil, ffmpeg.AVERROR(syscall.EINVAL)
|
|
}
|
|
|
|
// Allocate a codec context for the decoder
|
|
if decCtx = ffmpeg.AvCodecAllocContext3(dec); decCtx == nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to allocate the %s codec context\n",
|
|
ffmpeg.AvGetMediaTypeString(_type))
|
|
return 0, nil, ffmpeg.AVERROR(syscall.ENOMEM)
|
|
}
|
|
|
|
// Copy codec parameters from input stream to output codec context
|
|
if ret = ffmpeg.AvCodecParametersToContext(decCtx, st.GetCodecpar()); ret < 0 {
|
|
fmt.Fprintf(os.Stderr, "Failed to copy %s codec parameters to decoder context\n",
|
|
ffmpeg.AvGetMediaTypeString(_type))
|
|
return 0, nil, ret
|
|
}
|
|
|
|
// Init the decoders
|
|
if ret = ffmpeg.AvCodecOpen2(decCtx, dec, &opts); ret < 0 {
|
|
fmt.Fprintf(os.Stderr, "Failed to open %s codec\n",
|
|
ffmpeg.AvGetMediaTypeString(_type))
|
|
return 0, nil, ret
|
|
}
|
|
}
|
|
|
|
return streamIdx, decCtx, 0
|
|
}
|
|
|
|
func getFormatFromSampleFmt(sampleFmt ffmpeg.AVSampleFormat) (string, int32) {
|
|
sampleFmtEntry := []struct {
|
|
sampleFmt ffmpeg.AVSampleFormat
|
|
fmtBe string
|
|
fmtLe string
|
|
}{
|
|
{ffmpeg.AV_SAMPLE_FMT_U8, "u8", "u8"},
|
|
{ffmpeg.AV_SAMPLE_FMT_S16, "s16be", "s16le"},
|
|
{ffmpeg.AV_SAMPLE_FMT_S32, "s32be", "s32le"},
|
|
{ffmpeg.AV_SAMPLE_FMT_FLT, "f32be", "f32le"},
|
|
{ffmpeg.AV_SAMPLE_FMT_DBL, "f64be", "f64le"},
|
|
}
|
|
|
|
for _, entry := range sampleFmtEntry {
|
|
if sampleFmt == entry.sampleFmt {
|
|
return ffmpeg.AV_NE(entry.fmtBe, entry.fmtLe), 0
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "sample format %s is not supported as output format\n",
|
|
ffmpeg.AvGetSampleFmtName(sampleFmt))
|
|
return ffmpeg.NIL, -1
|
|
}
|
|
|
|
func main() {
|
|
var (
|
|
ret int32
|
|
err error
|
|
fmtCtx *ffmpeg.AVFormatContext
|
|
audioDecCtx *ffmpeg.AVCodecContext
|
|
videoDecCtx *ffmpeg.AVCodecContext
|
|
frame *ffmpeg.AVFrame
|
|
pkt *ffmpeg.AVPacket
|
|
)
|
|
if len(os.Args) != 4 {
|
|
fmt.Fprintf(os.Stderr, "usage: %s input_file video_output_file audio_output_file\n"+
|
|
"API example program to show how to read frames from an input file.\n"+
|
|
"This program reads frames from a file, decodes them, and writes decoded\n"+
|
|
"video frames to a rawvideo file named video_output_file, and decoded\n"+
|
|
"audio frames to a rawaudio file named audio_output_file.\n", os.Args[0])
|
|
os.Exit(1)
|
|
}
|
|
|
|
srcFilename = os.Args[1]
|
|
videoDstFilename := os.Args[2]
|
|
audioDstFilename := os.Args[3]
|
|
|
|
// open input file, and allocate format context
|
|
if ret = ffmpeg.AvFormatOpenInput(&fmtCtx, srcFilename, nil, nil); ret < 0 {
|
|
fmt.Fprintf(os.Stderr, "Could not open source file %s\n", srcFilename)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// retrieve stream information
|
|
if videoStreamIdx, videoDecCtx, ret = openCodecContext(fmtCtx, ffmpeg.AVMEDIA_TYPE_VIDEO); ret >= 0 {
|
|
videoStream = fmtCtx.GetStreams()[videoStreamIdx]
|
|
|
|
videoDstFile, err = os.OpenFile(videoDstFilename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Could not open destination file %s\n", videoDstFilename)
|
|
ret = 1
|
|
goto end
|
|
}
|
|
|
|
// allocate image where the decoded image will be put
|
|
width = videoDecCtx.GetWidth()
|
|
height = videoDecCtx.GetHeight()
|
|
pixFmt = videoDecCtx.GetPixFmt()
|
|
if pixFmt == ffmpeg.AV_PIX_FMT_NONE {
|
|
pixFmt = ffmpeg.AV_PIX_FMT_YUV420P
|
|
}
|
|
if ret = ffmpeg.AvImageAlloc(videoDstData[:], videoDstLinesize[:],
|
|
width, height, pixFmt, 1); ret < 0 {
|
|
fmt.Fprintf(os.Stderr, "Could not allocate raw video buffer (%s)\n", ffmpeg.AvErr2str(ret))
|
|
goto end
|
|
}
|
|
videoDstBufSize = ret
|
|
}
|
|
|
|
if audioStreamIdx, audioDecCtx, ret = openCodecContext(fmtCtx, ffmpeg.AVMEDIA_TYPE_AUDIO); ret >= 0 {
|
|
audioStream = fmtCtx.GetStreams()[audioStreamIdx]
|
|
|
|
audioDstFile, err = os.OpenFile(audioDstFilename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Could not open destination file %s\n", audioDstFilename)
|
|
ret = 1
|
|
goto end
|
|
}
|
|
}
|
|
|
|
// dump input information to stderr
|
|
ffmpeg.AvDumpFormat(fmtCtx, 0, srcFilename, 0)
|
|
|
|
if audioStream == nil && videoStream == nil {
|
|
fmt.Fprintf(os.Stderr, "Could not find audio or video stream in the input, aborting\n")
|
|
ret = 1
|
|
goto end
|
|
}
|
|
|
|
if frame = ffmpeg.AvFrameAlloc(); frame == nil {
|
|
fmt.Fprintf(os.Stderr, "Could not allocate frame\n")
|
|
ret = ffmpeg.AVERROR(syscall.ENOMEM)
|
|
goto end
|
|
}
|
|
|
|
if pkt = ffmpeg.AvPacketAlloc(); pkt == nil {
|
|
fmt.Fprintf(os.Stderr, "Could not allocate packet\n")
|
|
ret = ffmpeg.AVERROR(syscall.ENOMEM)
|
|
goto end
|
|
}
|
|
|
|
if videoStream != nil {
|
|
fmt.Fprintf(os.Stdout, "Demuxing video from file '%s' into '%s'\n", srcFilename, videoDstFilename)
|
|
}
|
|
if audioStream != nil {
|
|
fmt.Fprintf(os.Stdout, "Demuxing audio from file '%s' into '%s'\n", srcFilename, audioDstFilename)
|
|
}
|
|
|
|
// read frames from the file
|
|
for ffmpeg.AvReadFrame(fmtCtx, pkt) >= 0 {
|
|
// check if the packet belongs to a stream we are interested in, otherwise
|
|
// skip it
|
|
if pkt.GetStreamIndex() == videoStreamIdx {
|
|
ret = deocdePacket(videoDecCtx, pkt, frame)
|
|
} else if pkt.GetStreamIndex() == audioStreamIdx {
|
|
ret = deocdePacket(audioDecCtx, pkt, frame)
|
|
}
|
|
ffmpeg.AvPacketUnref(pkt)
|
|
if ret < 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
// flush the decoders
|
|
if videoDecCtx != nil {
|
|
deocdePacket(videoDecCtx, nil, frame)
|
|
}
|
|
if audioDecCtx != nil {
|
|
deocdePacket(audioDecCtx, nil, frame)
|
|
}
|
|
|
|
fmt.Fprintf(os.Stdout, "Demuxing succeeded.\n")
|
|
|
|
if videoStream != nil {
|
|
fmt.Fprintf(os.Stdout, "Play the output video file with the command:\n"+
|
|
"ffplay -f rawvideo -pixel_format %s -video_size %dx%d %s\n",
|
|
ffmpeg.AvGetPixFmtName(videoDecCtx.GetPixFmt()),
|
|
videoDecCtx.GetWidth(), videoDecCtx.GetHeight(), videoDstFilename)
|
|
}
|
|
|
|
if audioStream != nil {
|
|
sfmt := audioDecCtx.GetSampleFmt()
|
|
nChannels := audioDecCtx.GetChannels()
|
|
var _fmt string
|
|
|
|
if ffmpeg.AvSampleFmtIsPlanar(sfmt) != 0 {
|
|
packed := ffmpeg.AvGetSampleFmtName(sfmt)
|
|
if len(packed) == 0 {
|
|
packed = "?"
|
|
}
|
|
fmt.Fprintf(os.Stdout, "Warning: the sample format the decoder produced is planar "+
|
|
"(%s). This example will output the first channel only.\n", packed)
|
|
sfmt = ffmpeg.AvGetPackedSampleFmt(sfmt)
|
|
nChannels = 1
|
|
}
|
|
|
|
if _fmt, ret = getFormatFromSampleFmt(sfmt); ret < 0 {
|
|
goto end
|
|
}
|
|
|
|
fmt.Fprintf(os.Stdout, "Play the output audio file with the command:\n"+
|
|
"ffplay -f %s -ac %d -ar %d %s\n",
|
|
_fmt, nChannels, audioDecCtx.GetSampleRate(), audioDstFilename)
|
|
}
|
|
|
|
end:
|
|
ffmpeg.AvCodecFreeContext(&videoDecCtx)
|
|
ffmpeg.AvCodecFreeContext(&audioDecCtx)
|
|
ffmpeg.AvFormatCloseInput(&fmtCtx)
|
|
if videoDstFile != nil {
|
|
videoDstFile.Close()
|
|
}
|
|
if audioDstFile != nil {
|
|
audioDstFile.Close()
|
|
}
|
|
ffmpeg.AvPacketFree(&pkt)
|
|
ffmpeg.AvFrameFree(&frame)
|
|
ffmpeg.AvFree(videoDstData[0])
|
|
if ret < 0 {
|
|
os.Exit(int(ret))
|
|
}
|
|
}
|