first commit
This commit is contained in:
		
							
								
								
									
										758
									
								
								cgo/ffmpeg/audio.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										758
									
								
								cgo/ffmpeg/audio.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,758 @@ | ||||
| package ffmpeg | ||||
|  | ||||
| /* | ||||
| #include "ffmpeg.h" | ||||
| int wrap_avcodec_decode_audio4(AVCodecContext *ctx, AVFrame *frame, void *data, int size, int *got) { | ||||
| 	struct AVPacket pkt = {.data = data, .size = size}; | ||||
| 	return avcodec_decode_audio4(ctx, frame, got, &pkt); | ||||
| } | ||||
| int wrap_avresample_convert(AVAudioResampleContext *avr, int *out, int outsize, int outcount, int *in, int insize, int incount) { | ||||
| 	return avresample_convert(avr, (void *)out, outsize, outcount, (void *)in, insize, incount); | ||||
| } | ||||
| */ | ||||
| import "C" | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"runtime" | ||||
| 	"time" | ||||
| 	"unsafe" | ||||
|  | ||||
| 	"github.com/deepch/vdk/av" | ||||
| 	"github.com/deepch/vdk/av/avutil" | ||||
| 	"github.com/deepch/vdk/codec/aacparser" | ||||
| ) | ||||
|  | ||||
| const debug = false | ||||
|  | ||||
| type Resampler struct { | ||||
| 	inSampleFormat, OutSampleFormat   av.SampleFormat | ||||
| 	inChannelLayout, OutChannelLayout av.ChannelLayout | ||||
| 	inSampleRate, OutSampleRate       int | ||||
| 	avr                               *C.AVAudioResampleContext | ||||
| } | ||||
|  | ||||
| func (self *Resampler) Resample(in av.AudioFrame) (out av.AudioFrame, err error) { | ||||
| 	formatChange := in.SampleRate != self.inSampleRate || in.SampleFormat != self.inSampleFormat || in.ChannelLayout != self.inChannelLayout | ||||
|  | ||||
| 	var flush av.AudioFrame | ||||
|  | ||||
| 	if formatChange { | ||||
| 		if self.avr != nil { | ||||
| 			outChannels := self.OutChannelLayout.Count() | ||||
| 			if !self.OutSampleFormat.IsPlanar() { | ||||
| 				outChannels = 1 | ||||
| 			} | ||||
| 			outData := make([]*C.uint8_t, outChannels) | ||||
| 			outSampleCount := int(C.avresample_get_out_samples(self.avr, C.int(in.SampleCount))) | ||||
| 			outLinesize := outSampleCount * self.OutSampleFormat.BytesPerSample() | ||||
| 			flush.Data = make([][]byte, outChannels) | ||||
| 			for i := 0; i < outChannels; i++ { | ||||
| 				flush.Data[i] = make([]byte, outLinesize) | ||||
| 				outData[i] = (*C.uint8_t)(unsafe.Pointer(&flush.Data[i][0])) | ||||
| 			} | ||||
| 			flush.ChannelLayout = self.OutChannelLayout | ||||
| 			flush.SampleFormat = self.OutSampleFormat | ||||
| 			flush.SampleRate = self.OutSampleRate | ||||
|  | ||||
| 			convertSamples := int(C.wrap_avresample_convert( | ||||
| 				self.avr, | ||||
| 				(*C.int)(unsafe.Pointer(&outData[0])), C.int(outLinesize), C.int(outSampleCount), | ||||
| 				nil, C.int(0), C.int(0), | ||||
| 			)) | ||||
| 			if convertSamples < 0 { | ||||
| 				err = fmt.Errorf("ffmpeg: avresample_convert_frame failed") | ||||
| 				return | ||||
| 			} | ||||
| 			flush.SampleCount = convertSamples | ||||
| 			if convertSamples < outSampleCount { | ||||
| 				for i := 0; i < outChannels; i++ { | ||||
| 					flush.Data[i] = flush.Data[i][:convertSamples*self.OutSampleFormat.BytesPerSample()] | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			//fmt.Println("flush:", "outSampleCount", outSampleCount, "convertSamples", convertSamples, "datasize", len(flush.Data[0])) | ||||
| 		} else { | ||||
| 			runtime.SetFinalizer(self, func(self *Resampler) { | ||||
| 				self.Close() | ||||
| 			}) | ||||
| 		} | ||||
|  | ||||
| 		C.avresample_free(&self.avr) | ||||
| 		self.inSampleFormat = in.SampleFormat | ||||
| 		self.inSampleRate = in.SampleRate | ||||
| 		self.inChannelLayout = in.ChannelLayout | ||||
| 		avr := C.avresample_alloc_context() | ||||
| 		C.av_opt_set_int(unsafe.Pointer(avr), C.CString("in_channel_layout"), C.int64_t(channelLayoutAV2FF(self.inChannelLayout)), 0) | ||||
| 		C.av_opt_set_int(unsafe.Pointer(avr), C.CString("out_channel_layout"), C.int64_t(channelLayoutAV2FF(self.OutChannelLayout)), 0) | ||||
| 		C.av_opt_set_int(unsafe.Pointer(avr), C.CString("in_sample_rate"), C.int64_t(self.inSampleRate), 0) | ||||
| 		C.av_opt_set_int(unsafe.Pointer(avr), C.CString("out_sample_rate"), C.int64_t(self.OutSampleRate), 0) | ||||
| 		C.av_opt_set_int(unsafe.Pointer(avr), C.CString("in_sample_fmt"), C.int64_t(sampleFormatAV2FF(self.inSampleFormat)), 0) | ||||
| 		C.av_opt_set_int(unsafe.Pointer(avr), C.CString("out_sample_fmt"), C.int64_t(sampleFormatAV2FF(self.OutSampleFormat)), 0) | ||||
| 		C.avresample_open(avr) | ||||
| 		self.avr = avr | ||||
| 	} | ||||
|  | ||||
| 	var inChannels, inLinesize int | ||||
| 	inSampleCount := in.SampleCount | ||||
| 	if !self.inSampleFormat.IsPlanar() { | ||||
| 		inChannels = 1 | ||||
| 		inLinesize = inSampleCount * in.SampleFormat.BytesPerSample() * self.inChannelLayout.Count() | ||||
| 	} else { | ||||
| 		inChannels = self.inChannelLayout.Count() | ||||
| 		inLinesize = inSampleCount * in.SampleFormat.BytesPerSample() | ||||
| 	} | ||||
| 	inData := make([]*C.uint8_t, inChannels) | ||||
| 	for i := 0; i < inChannels; i++ { | ||||
| 		inData[i] = (*C.uint8_t)(unsafe.Pointer(&in.Data[i][0])) | ||||
| 	} | ||||
|  | ||||
| 	var outChannels, outLinesize, outBytesPerSample int | ||||
| 	outSampleCount := int(C.avresample_get_out_samples(self.avr, C.int(in.SampleCount))) | ||||
| 	if !self.OutSampleFormat.IsPlanar() { | ||||
| 		outChannels = 1 | ||||
| 		outBytesPerSample = self.OutSampleFormat.BytesPerSample() * self.OutChannelLayout.Count() | ||||
| 		outLinesize = outSampleCount * outBytesPerSample | ||||
| 	} else { | ||||
| 		outChannels = self.OutChannelLayout.Count() | ||||
| 		outBytesPerSample = self.OutSampleFormat.BytesPerSample() | ||||
| 		outLinesize = outSampleCount * outBytesPerSample | ||||
| 	} | ||||
| 	outData := make([]*C.uint8_t, outChannels) | ||||
| 	out.Data = make([][]byte, outChannels) | ||||
| 	for i := 0; i < outChannels; i++ { | ||||
| 		out.Data[i] = make([]byte, outLinesize) | ||||
| 		outData[i] = (*C.uint8_t)(unsafe.Pointer(&out.Data[i][0])) | ||||
| 	} | ||||
| 	out.ChannelLayout = self.OutChannelLayout | ||||
| 	out.SampleFormat = self.OutSampleFormat | ||||
| 	out.SampleRate = self.OutSampleRate | ||||
|  | ||||
| 	convertSamples := int(C.wrap_avresample_convert( | ||||
| 		self.avr, | ||||
| 		(*C.int)(unsafe.Pointer(&outData[0])), C.int(outLinesize), C.int(outSampleCount), | ||||
| 		(*C.int)(unsafe.Pointer(&inData[0])), C.int(inLinesize), C.int(inSampleCount), | ||||
| 	)) | ||||
| 	if convertSamples < 0 { | ||||
| 		err = fmt.Errorf("ffmpeg: avresample_convert_frame failed") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	out.SampleCount = convertSamples | ||||
| 	if convertSamples < outSampleCount { | ||||
| 		for i := 0; i < outChannels; i++ { | ||||
| 			out.Data[i] = out.Data[i][:convertSamples*outBytesPerSample] | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if flush.SampleCount > 0 { | ||||
| 		out = flush.Concat(out) | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (self *Resampler) Close() { | ||||
| 	C.avresample_free(&self.avr) | ||||
| } | ||||
|  | ||||
| type AudioEncoder struct { | ||||
| 	ff               *ffctx | ||||
| 	SampleRate       int | ||||
| 	Bitrate          int | ||||
| 	ChannelLayout    av.ChannelLayout | ||||
| 	SampleFormat     av.SampleFormat | ||||
| 	FrameSampleCount int | ||||
| 	framebuf         av.AudioFrame | ||||
| 	codecData        av.AudioCodecData | ||||
| 	resampler        *Resampler | ||||
| } | ||||
|  | ||||
| func sampleFormatAV2FF(sampleFormat av.SampleFormat) (ffsamplefmt int32) { | ||||
| 	switch sampleFormat { | ||||
| 	case av.U8: | ||||
| 		ffsamplefmt = C.AV_SAMPLE_FMT_U8 | ||||
| 	case av.S16: | ||||
| 		ffsamplefmt = C.AV_SAMPLE_FMT_S16 | ||||
| 	case av.S32: | ||||
| 		ffsamplefmt = C.AV_SAMPLE_FMT_S32 | ||||
| 	case av.FLT: | ||||
| 		ffsamplefmt = C.AV_SAMPLE_FMT_FLT | ||||
| 	case av.DBL: | ||||
| 		ffsamplefmt = C.AV_SAMPLE_FMT_DBL | ||||
| 	case av.U8P: | ||||
| 		ffsamplefmt = C.AV_SAMPLE_FMT_U8P | ||||
| 	case av.S16P: | ||||
| 		ffsamplefmt = C.AV_SAMPLE_FMT_S16P | ||||
| 	case av.S32P: | ||||
| 		ffsamplefmt = C.AV_SAMPLE_FMT_S32P | ||||
| 	case av.FLTP: | ||||
| 		ffsamplefmt = C.AV_SAMPLE_FMT_FLTP | ||||
| 	case av.DBLP: | ||||
| 		ffsamplefmt = C.AV_SAMPLE_FMT_DBLP | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func sampleFormatFF2AV(ffsamplefmt int32) (sampleFormat av.SampleFormat) { | ||||
| 	switch ffsamplefmt { | ||||
| 	case C.AV_SAMPLE_FMT_U8: ///< unsigned 8 bits | ||||
| 		sampleFormat = av.U8 | ||||
| 	case C.AV_SAMPLE_FMT_S16: ///< signed 16 bits | ||||
| 		sampleFormat = av.S16 | ||||
| 	case C.AV_SAMPLE_FMT_S32: ///< signed 32 bits | ||||
| 		sampleFormat = av.S32 | ||||
| 	case C.AV_SAMPLE_FMT_FLT: ///< float | ||||
| 		sampleFormat = av.FLT | ||||
| 	case C.AV_SAMPLE_FMT_DBL: ///< double | ||||
| 		sampleFormat = av.DBL | ||||
| 	case C.AV_SAMPLE_FMT_U8P: ///< unsigned 8 bits, planar | ||||
| 		sampleFormat = av.U8P | ||||
| 	case C.AV_SAMPLE_FMT_S16P: ///< signed 16 bits, planar | ||||
| 		sampleFormat = av.S16P | ||||
| 	case C.AV_SAMPLE_FMT_S32P: ///< signed 32 bits, planar | ||||
| 		sampleFormat = av.S32P | ||||
| 	case C.AV_SAMPLE_FMT_FLTP: ///< float, planar | ||||
| 		sampleFormat = av.FLTP | ||||
| 	case C.AV_SAMPLE_FMT_DBLP: ///< double, planar | ||||
| 		sampleFormat = av.DBLP | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (self *AudioEncoder) SetSampleFormat(fmt av.SampleFormat) (err error) { | ||||
| 	self.SampleFormat = fmt | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (self *AudioEncoder) SetSampleRate(rate int) (err error) { | ||||
| 	self.SampleRate = rate | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (self *AudioEncoder) SetChannelLayout(ch av.ChannelLayout) (err error) { | ||||
| 	self.ChannelLayout = ch | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (self *AudioEncoder) SetBitrate(bitrate int) (err error) { | ||||
| 	self.Bitrate = bitrate | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (self *AudioEncoder) SetOption(key string, val interface{}) (err error) { | ||||
| 	ff := &self.ff.ff | ||||
|  | ||||
| 	sval := fmt.Sprint(val) | ||||
| 	if key == "profile" { | ||||
| 		ff.profile = C.avcodec_profile_name_to_int(ff.codec, C.CString(sval)) | ||||
| 		if ff.profile == C.FF_PROFILE_UNKNOWN { | ||||
| 			err = fmt.Errorf("ffmpeg: profile `%s` invalid", sval) | ||||
| 			return | ||||
| 		} | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	C.av_dict_set(&ff.options, C.CString(key), C.CString(sval), 0) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (self *AudioEncoder) GetOption(key string, val interface{}) (err error) { | ||||
| 	ff := &self.ff.ff | ||||
| 	entry := C.av_dict_get(ff.options, C.CString(key), nil, 0) | ||||
| 	if entry == nil { | ||||
| 		err = fmt.Errorf("ffmpeg: GetOption failed: `%s` not exists", key) | ||||
| 		return | ||||
| 	} | ||||
| 	switch p := val.(type) { | ||||
| 	case *string: | ||||
| 		*p = C.GoString(entry.value) | ||||
| 	case *int: | ||||
| 		fmt.Sscanf(C.GoString(entry.value), "%d", p) | ||||
| 	default: | ||||
| 		err = fmt.Errorf("ffmpeg: GetOption failed: val must be *string or *int receiver") | ||||
| 		return | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (self *AudioEncoder) Setup() (err error) { | ||||
| 	ff := &self.ff.ff | ||||
|  | ||||
| 	ff.frame = C.av_frame_alloc() | ||||
|  | ||||
| 	if self.SampleFormat == av.SampleFormat(0) { | ||||
| 		self.SampleFormat = sampleFormatFF2AV(*ff.codec.sample_fmts) | ||||
| 	} | ||||
|  | ||||
| 	//if self.Bitrate == 0 { | ||||
| 	//	self.Bitrate = 80000 | ||||
| 	//} | ||||
| 	if self.SampleRate == 0 { | ||||
| 		self.SampleRate = 44100 | ||||
| 	} | ||||
| 	if self.ChannelLayout == av.ChannelLayout(0) { | ||||
| 		self.ChannelLayout = av.CH_STEREO | ||||
| 	} | ||||
|  | ||||
| 	ff.codecCtx.sample_fmt = sampleFormatAV2FF(self.SampleFormat) | ||||
| 	ff.codecCtx.sample_rate = C.int(self.SampleRate) | ||||
| 	ff.codecCtx.bit_rate = C.int64_t(self.Bitrate) | ||||
| 	ff.codecCtx.channel_layout = channelLayoutAV2FF(self.ChannelLayout) | ||||
| 	ff.codecCtx.strict_std_compliance = C.FF_COMPLIANCE_EXPERIMENTAL | ||||
| 	ff.codecCtx.flags = C.AV_CODEC_FLAG_GLOBAL_HEADER | ||||
| 	ff.codecCtx.profile = ff.profile | ||||
|  | ||||
| 	if C.avcodec_open2(ff.codecCtx, ff.codec, nil) != 0 { | ||||
| 		err = fmt.Errorf("ffmpeg: encoder: avcodec_open2 failed") | ||||
| 		return | ||||
| 	} | ||||
| 	self.SampleFormat = sampleFormatFF2AV(ff.codecCtx.sample_fmt) | ||||
| 	self.FrameSampleCount = int(ff.codecCtx.frame_size) | ||||
|  | ||||
| 	extradata := C.GoBytes(unsafe.Pointer(ff.codecCtx.extradata), ff.codecCtx.extradata_size) | ||||
|  | ||||
| 	switch ff.codecCtx.codec_id { | ||||
| 	case C.AV_CODEC_ID_AAC: | ||||
| 		if self.codecData, err = aacparser.NewCodecDataFromMPEG4AudioConfigBytes(extradata); err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 	default: | ||||
| 		self.codecData = audioCodecData{ | ||||
| 			channelLayout: self.ChannelLayout, | ||||
| 			sampleFormat:  self.SampleFormat, | ||||
| 			sampleRate:    self.SampleRate, | ||||
| 			codecId:       ff.codecCtx.codec_id, | ||||
| 			extradata:     extradata, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (self *AudioEncoder) prepare() (err error) { | ||||
| 	ff := &self.ff.ff | ||||
|  | ||||
| 	if ff.frame == nil { | ||||
| 		if err = self.Setup(); err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (self *AudioEncoder) CodecData() (codec av.AudioCodecData, err error) { | ||||
| 	if err = self.prepare(); err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	codec = self.codecData | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (self *AudioEncoder) encodeOne(frame av.AudioFrame) (gotpkt bool, pkt []byte, err error) { | ||||
| 	if err = self.prepare(); err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ff := &self.ff.ff | ||||
|  | ||||
| 	cpkt := C.AVPacket{} | ||||
| 	cgotpkt := C.int(0) | ||||
| 	audioFrameAssignToFF(frame, ff.frame) | ||||
|  | ||||
| 	if false { | ||||
| 		farr := []string{} | ||||
| 		for i := 0; i < len(frame.Data[0])/4; i++ { | ||||
| 			var f *float64 = (*float64)(unsafe.Pointer(&frame.Data[0][i*4])) | ||||
| 			farr = append(farr, fmt.Sprintf("%.8f", *f)) | ||||
| 		} | ||||
| 		fmt.Println(farr) | ||||
| 	} | ||||
| 	cerr := C.avcodec_encode_audio2(ff.codecCtx, &cpkt, ff.frame, &cgotpkt) | ||||
| 	if cerr < C.int(0) { | ||||
| 		err = fmt.Errorf("ffmpeg: avcodec_encode_audio2 failed: %d", cerr) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if cgotpkt != 0 { | ||||
| 		gotpkt = true | ||||
| 		pkt = C.GoBytes(unsafe.Pointer(cpkt.data), cpkt.size) | ||||
| 		C.av_packet_unref(&cpkt) | ||||
|  | ||||
| 		if debug { | ||||
| 			fmt.Println("ffmpeg: Encode", frame.SampleCount, frame.SampleRate, frame.ChannelLayout, frame.SampleFormat, "len", len(pkt)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (self *AudioEncoder) resample(in av.AudioFrame) (out av.AudioFrame, err error) { | ||||
| 	if self.resampler == nil { | ||||
| 		self.resampler = &Resampler{ | ||||
| 			OutSampleFormat:  self.SampleFormat, | ||||
| 			OutSampleRate:    self.SampleRate, | ||||
| 			OutChannelLayout: self.ChannelLayout, | ||||
| 		} | ||||
| 	} | ||||
| 	if out, err = self.resampler.Resample(in); err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (self *AudioEncoder) Encode(frame av.AudioFrame) (pkts [][]byte, err error) { | ||||
| 	var gotpkt bool | ||||
| 	var pkt []byte | ||||
|  | ||||
| 	if frame.SampleFormat != self.SampleFormat || frame.ChannelLayout != self.ChannelLayout || frame.SampleRate != self.SampleRate { | ||||
| 		if frame, err = self.resample(frame); err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if self.FrameSampleCount != 0 { | ||||
| 		if self.framebuf.SampleCount == 0 { | ||||
| 			self.framebuf = frame | ||||
| 		} else { | ||||
| 			self.framebuf = self.framebuf.Concat(frame) | ||||
| 		} | ||||
| 		for self.framebuf.SampleCount >= self.FrameSampleCount { | ||||
| 			frame := self.framebuf.Slice(0, self.FrameSampleCount) | ||||
| 			if gotpkt, pkt, err = self.encodeOne(frame); err != nil { | ||||
| 				return | ||||
| 			} | ||||
| 			if gotpkt { | ||||
| 				pkts = append(pkts, pkt) | ||||
| 			} | ||||
| 			self.framebuf = self.framebuf.Slice(self.FrameSampleCount, self.framebuf.SampleCount) | ||||
| 		} | ||||
| 	} else { | ||||
| 		if gotpkt, pkt, err = self.encodeOne(frame); err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 		if gotpkt { | ||||
| 			pkts = append(pkts, pkt) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (self *AudioEncoder) Close() { | ||||
| 	freeFFCtx(self.ff) | ||||
| 	if self.resampler != nil { | ||||
| 		self.resampler.Close() | ||||
| 		self.resampler = nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func audioFrameAssignToAVParams(f *C.AVFrame, frame *av.AudioFrame) { | ||||
| 	frame.SampleFormat = sampleFormatFF2AV(int32(f.format)) | ||||
| 	frame.ChannelLayout = channelLayoutFF2AV(f.channel_layout) | ||||
| 	frame.SampleRate = int(f.sample_rate) | ||||
| } | ||||
|  | ||||
| func audioFrameAssignToAVData(f *C.AVFrame, frame *av.AudioFrame) { | ||||
| 	frame.SampleCount = int(f.nb_samples) | ||||
| 	frame.Data = make([][]byte, int(f.channels)) | ||||
| 	for i := 0; i < int(f.channels); i++ { | ||||
| 		frame.Data[i] = C.GoBytes(unsafe.Pointer(f.data[i]), f.linesize[0]) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func audioFrameAssignToAV(f *C.AVFrame, frame *av.AudioFrame) { | ||||
| 	audioFrameAssignToAVParams(f, frame) | ||||
| 	audioFrameAssignToAVData(f, frame) | ||||
| } | ||||
|  | ||||
| func audioFrameAssignToFFParams(frame av.AudioFrame, f *C.AVFrame) { | ||||
| 	f.format = C.int(sampleFormatAV2FF(frame.SampleFormat)) | ||||
| 	f.channel_layout = channelLayoutAV2FF(frame.ChannelLayout) | ||||
| 	f.sample_rate = C.int(frame.SampleRate) | ||||
| 	f.channels = C.int(frame.ChannelLayout.Count()) | ||||
| } | ||||
|  | ||||
| func audioFrameAssignToFFData(frame av.AudioFrame, f *C.AVFrame) { | ||||
| 	f.nb_samples = C.int(frame.SampleCount) | ||||
| 	for i := range frame.Data { | ||||
| 		f.data[i] = (*C.uint8_t)(unsafe.Pointer(&frame.Data[i][0])) | ||||
| 		f.linesize[i] = C.int(len(frame.Data[i])) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func audioFrameAssignToFF(frame av.AudioFrame, f *C.AVFrame) { | ||||
| 	audioFrameAssignToFFParams(frame, f) | ||||
| 	audioFrameAssignToFFData(frame, f) | ||||
| } | ||||
|  | ||||
| func channelLayoutFF2AV(layout C.uint64_t) (channelLayout av.ChannelLayout) { | ||||
| 	if layout&C.AV_CH_FRONT_CENTER != 0 { | ||||
| 		channelLayout |= av.CH_FRONT_CENTER | ||||
| 	} | ||||
| 	if layout&C.AV_CH_FRONT_LEFT != 0 { | ||||
| 		channelLayout |= av.CH_FRONT_LEFT | ||||
| 	} | ||||
| 	if layout&C.AV_CH_FRONT_RIGHT != 0 { | ||||
| 		channelLayout |= av.CH_FRONT_RIGHT | ||||
| 	} | ||||
| 	if layout&C.AV_CH_BACK_CENTER != 0 { | ||||
| 		channelLayout |= av.CH_BACK_CENTER | ||||
| 	} | ||||
| 	if layout&C.AV_CH_BACK_LEFT != 0 { | ||||
| 		channelLayout |= av.CH_BACK_LEFT | ||||
| 	} | ||||
| 	if layout&C.AV_CH_BACK_RIGHT != 0 { | ||||
| 		channelLayout |= av.CH_BACK_RIGHT | ||||
| 	} | ||||
| 	if layout&C.AV_CH_SIDE_LEFT != 0 { | ||||
| 		channelLayout |= av.CH_SIDE_LEFT | ||||
| 	} | ||||
| 	if layout&C.AV_CH_SIDE_RIGHT != 0 { | ||||
| 		channelLayout |= av.CH_SIDE_RIGHT | ||||
| 	} | ||||
| 	if layout&C.AV_CH_LOW_FREQUENCY != 0 { | ||||
| 		channelLayout |= av.CH_LOW_FREQ | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func channelLayoutAV2FF(channelLayout av.ChannelLayout) (layout C.uint64_t) { | ||||
| 	if channelLayout&av.CH_FRONT_CENTER != 0 { | ||||
| 		layout |= C.AV_CH_FRONT_CENTER | ||||
| 	} | ||||
| 	if channelLayout&av.CH_FRONT_LEFT != 0 { | ||||
| 		layout |= C.AV_CH_FRONT_LEFT | ||||
| 	} | ||||
| 	if channelLayout&av.CH_FRONT_RIGHT != 0 { | ||||
| 		layout |= C.AV_CH_FRONT_RIGHT | ||||
| 	} | ||||
| 	if channelLayout&av.CH_BACK_CENTER != 0 { | ||||
| 		layout |= C.AV_CH_BACK_CENTER | ||||
| 	} | ||||
| 	if channelLayout&av.CH_BACK_LEFT != 0 { | ||||
| 		layout |= C.AV_CH_BACK_LEFT | ||||
| 	} | ||||
| 	if channelLayout&av.CH_BACK_RIGHT != 0 { | ||||
| 		layout |= C.AV_CH_BACK_RIGHT | ||||
| 	} | ||||
| 	if channelLayout&av.CH_SIDE_LEFT != 0 { | ||||
| 		layout |= C.AV_CH_SIDE_LEFT | ||||
| 	} | ||||
| 	if channelLayout&av.CH_SIDE_RIGHT != 0 { | ||||
| 		layout |= C.AV_CH_SIDE_RIGHT | ||||
| 	} | ||||
| 	if channelLayout&av.CH_LOW_FREQ != 0 { | ||||
| 		layout |= C.AV_CH_LOW_FREQUENCY | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| type AudioDecoder struct { | ||||
| 	ff            *ffctx | ||||
| 	ChannelLayout av.ChannelLayout | ||||
| 	SampleFormat  av.SampleFormat | ||||
| 	SampleRate    int | ||||
| 	Extradata     []byte | ||||
| } | ||||
|  | ||||
| func (self *AudioDecoder) Setup() (err error) { | ||||
| 	ff := &self.ff.ff | ||||
|  | ||||
| 	ff.frame = C.av_frame_alloc() | ||||
|  | ||||
| 	if len(self.Extradata) > 0 { | ||||
| 		ff.codecCtx.extradata = (*C.uint8_t)(unsafe.Pointer(&self.Extradata[0])) | ||||
| 		ff.codecCtx.extradata_size = C.int(len(self.Extradata)) | ||||
| 	} | ||||
| 	if debug { | ||||
| 		fmt.Println("ffmpeg: Decoder.Setup Extradata.len", len(self.Extradata)) | ||||
| 	} | ||||
|  | ||||
| 	ff.codecCtx.sample_rate = C.int(self.SampleRate) | ||||
| 	ff.codecCtx.channel_layout = channelLayoutAV2FF(self.ChannelLayout) | ||||
| 	ff.codecCtx.channels = C.int(self.ChannelLayout.Count()) | ||||
| 	if C.avcodec_open2(ff.codecCtx, ff.codec, nil) != 0 { | ||||
| 		err = fmt.Errorf("ffmpeg: decoder: avcodec_open2 failed") | ||||
| 		return | ||||
| 	} | ||||
| 	self.SampleFormat = sampleFormatFF2AV(ff.codecCtx.sample_fmt) | ||||
| 	self.ChannelLayout = channelLayoutFF2AV(ff.codecCtx.channel_layout) | ||||
| 	if self.SampleRate == 0 { | ||||
| 		self.SampleRate = int(ff.codecCtx.sample_rate) | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (self *AudioDecoder) Decode(pkt []byte) (gotframe bool, frame av.AudioFrame, err error) { | ||||
| 	ff := &self.ff.ff | ||||
|  | ||||
| 	cgotframe := C.int(0) | ||||
| 	cerr := C.wrap_avcodec_decode_audio4(ff.codecCtx, ff.frame, unsafe.Pointer(&pkt[0]), C.int(len(pkt)), &cgotframe) | ||||
| 	if cerr < C.int(0) { | ||||
| 		err = fmt.Errorf("ffmpeg: avcodec_decode_audio4 failed: %d", cerr) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if cgotframe != C.int(0) { | ||||
| 		gotframe = true | ||||
| 		audioFrameAssignToAV(ff.frame, &frame) | ||||
| 		frame.SampleRate = self.SampleRate | ||||
|  | ||||
| 		if debug { | ||||
| 			fmt.Println("ffmpeg: Decode", frame.SampleCount, frame.SampleRate, frame.ChannelLayout, frame.SampleFormat) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (self *AudioDecoder) Close() { | ||||
| 	freeFFCtx(self.ff) | ||||
| } | ||||
|  | ||||
| func NewAudioEncoderByCodecType(typ av.CodecType) (enc *AudioEncoder, err error) { | ||||
| 	var id uint32 | ||||
|  | ||||
| 	switch typ { | ||||
| 	case av.AAC: | ||||
| 		id = C.AV_CODEC_ID_AAC | ||||
|  | ||||
| 	default: | ||||
| 		err = fmt.Errorf("ffmpeg: cannot find encoder codecType=%d", typ) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	codec := C.avcodec_find_encoder(id) | ||||
| 	if codec == nil || C.avcodec_get_type(id) != C.AVMEDIA_TYPE_AUDIO { | ||||
| 		err = fmt.Errorf("ffmpeg: cannot find audio encoder codecId=%d", id) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	_enc := &AudioEncoder{} | ||||
| 	if _enc.ff, err = newFFCtxByCodec(codec); err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	enc = _enc | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func NewAudioEncoderByName(name string) (enc *AudioEncoder, err error) { | ||||
| 	_enc := &AudioEncoder{} | ||||
|  | ||||
| 	codec := C.avcodec_find_encoder_by_name(C.CString(name)) | ||||
| 	if codec == nil || C.avcodec_get_type(codec.id) != C.AVMEDIA_TYPE_AUDIO { | ||||
| 		err = fmt.Errorf("ffmpeg: cannot find audio encoder name=%s", name) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if _enc.ff, err = newFFCtxByCodec(codec); err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	enc = _enc | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func NewAudioDecoder(codec av.AudioCodecData) (dec *AudioDecoder, err error) { | ||||
| 	_dec := &AudioDecoder{} | ||||
| 	var id uint32 | ||||
|  | ||||
| 	switch codec.Type() { | ||||
| 	case av.AAC: | ||||
| 		if aaccodec, ok := codec.(aacparser.CodecData); ok { | ||||
| 			_dec.Extradata = aaccodec.MPEG4AudioConfigBytes() | ||||
| 			id = C.AV_CODEC_ID_AAC | ||||
| 		} else { | ||||
| 			err = fmt.Errorf("ffmpeg: aac CodecData must be aacparser.CodecData") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 	case av.SPEEX: | ||||
| 		id = C.AV_CODEC_ID_SPEEX | ||||
|  | ||||
| 	case av.PCM_MULAW: | ||||
| 		id = C.AV_CODEC_ID_PCM_MULAW | ||||
|  | ||||
| 	case av.PCM_ALAW: | ||||
| 		id = C.AV_CODEC_ID_PCM_ALAW | ||||
|  | ||||
| 	default: | ||||
| 		if ffcodec, ok := codec.(audioCodecData); ok { | ||||
| 			_dec.Extradata = ffcodec.extradata | ||||
| 			id = ffcodec.codecId | ||||
| 		} else { | ||||
| 			err = fmt.Errorf("ffmpeg: invalid CodecData for ffmpeg to decode") | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	c := C.avcodec_find_decoder(id) | ||||
| 	if c == nil || C.avcodec_get_type(c.id) != C.AVMEDIA_TYPE_AUDIO { | ||||
| 		err = fmt.Errorf("ffmpeg: cannot find audio decoder id=%d", id) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if _dec.ff, err = newFFCtxByCodec(c); err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	_dec.SampleFormat = codec.SampleFormat() | ||||
| 	_dec.SampleRate = codec.SampleRate() | ||||
| 	_dec.ChannelLayout = codec.ChannelLayout() | ||||
| 	if err = _dec.Setup(); err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	dec = _dec | ||||
| 	return | ||||
| } | ||||
|  | ||||
| type audioCodecData struct { | ||||
| 	codecId       uint32 | ||||
| 	sampleFormat  av.SampleFormat | ||||
| 	channelLayout av.ChannelLayout | ||||
| 	sampleRate    int | ||||
| 	extradata     []byte | ||||
| } | ||||
|  | ||||
| func (self audioCodecData) Type() av.CodecType { | ||||
| 	return av.MakeAudioCodecType(self.codecId) | ||||
| } | ||||
|  | ||||
| func (self audioCodecData) SampleRate() int { | ||||
| 	return self.sampleRate | ||||
| } | ||||
|  | ||||
| func (self audioCodecData) SampleFormat() av.SampleFormat { | ||||
| 	return self.sampleFormat | ||||
| } | ||||
|  | ||||
| func (self audioCodecData) ChannelLayout() av.ChannelLayout { | ||||
| 	return self.channelLayout | ||||
| } | ||||
|  | ||||
| func (self audioCodecData) PacketDuration(data []byte) (dur time.Duration, err error) { | ||||
| 	// TODO: implement it: ffmpeg get_audio_frame_duration | ||||
| 	err = fmt.Errorf("ffmpeg: cannot get packet duration") | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func AudioCodecHandler(h *avutil.RegisterHandler) { | ||||
| 	h.AudioDecoder = func(codec av.AudioCodecData) (av.AudioDecoder, error) { | ||||
| 		if dec, err := NewAudioDecoder(codec); err != nil { | ||||
| 			return nil, nil | ||||
| 		} else { | ||||
| 			return dec, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	h.AudioEncoder = func(typ av.CodecType) (av.AudioEncoder, error) { | ||||
| 		if enc, err := NewAudioEncoderByCodecType(typ); err != nil { | ||||
| 			return nil, nil | ||||
| 		} else { | ||||
| 			return enc, err | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										74
									
								
								cgo/ffmpeg/ffmpeg.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								cgo/ffmpeg/ffmpeg.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| package ffmpeg | ||||
|  | ||||
| /* | ||||
| #cgo LDFLAGS: -lavformat -lavutil -lavcodec -lavresample -lswscale | ||||
| #include "ffmpeg.h" | ||||
| void ffinit() { | ||||
| 	av_register_all(); | ||||
| } | ||||
| */ | ||||
| import "C" | ||||
| import ( | ||||
| 	"runtime" | ||||
| 	"unsafe" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	QUIET = int(C.AV_LOG_QUIET) | ||||
| 	PANIC = int(C.AV_LOG_PANIC) | ||||
| 	FATAL = int(C.AV_LOG_FATAL) | ||||
| 	ERROR = int(C.AV_LOG_ERROR) | ||||
| 	WARNING = int(C.AV_LOG_WARNING) | ||||
| 	INFO = int(C.AV_LOG_INFO) | ||||
| 	VERBOSE = int(C.AV_LOG_VERBOSE) | ||||
| 	DEBUG = int(C.AV_LOG_DEBUG) | ||||
| 	TRACE = int(C.AV_LOG_TRACE) | ||||
| ) | ||||
|  | ||||
| func HasEncoder(name string) bool { | ||||
| 	return C.avcodec_find_encoder_by_name(C.CString(name)) != nil | ||||
| } | ||||
|  | ||||
| func HasDecoder(name string) bool { | ||||
| 	return C.avcodec_find_decoder_by_name(C.CString(name)) != nil | ||||
| } | ||||
|  | ||||
| //func EncodersList() []string | ||||
| //func DecodersList() []string | ||||
|  | ||||
| func SetLogLevel(level int) { | ||||
| 	C.av_log_set_level(C.int(level)) | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	C.ffinit() | ||||
| } | ||||
|  | ||||
| type ffctx struct { | ||||
| 	ff C.FFCtx | ||||
| } | ||||
|  | ||||
| func newFFCtxByCodec(codec *C.AVCodec) (ff *ffctx, err error) { | ||||
| 	ff = &ffctx{} | ||||
| 	ff.ff.codec = codec | ||||
| 	ff.ff.codecCtx = C.avcodec_alloc_context3(codec) | ||||
| 	ff.ff.profile = C.FF_PROFILE_UNKNOWN | ||||
| 	runtime.SetFinalizer(ff, freeFFCtx) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func freeFFCtx(self *ffctx) { | ||||
| 	ff := &self.ff | ||||
| 	if ff.frame != nil { | ||||
| 		C.av_frame_free(&ff.frame) | ||||
| 	} | ||||
| 	if ff.codecCtx != nil { | ||||
| 		C.avcodec_close(ff.codecCtx) | ||||
| 		C.av_free(unsafe.Pointer(ff.codecCtx)) | ||||
| 		ff.codecCtx = nil | ||||
| 	} | ||||
| 	if ff.options != nil { | ||||
| 		C.av_dict_free(&ff.options) | ||||
| 	} | ||||
| } | ||||
|  | ||||
							
								
								
									
										25
									
								
								cgo/ffmpeg/ffmpeg.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								cgo/ffmpeg/ffmpeg.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
|  | ||||
| #include <libavformat/avformat.h> | ||||
| #include <libavcodec/avcodec.h> | ||||
| #include <libavutil/avutil.h> | ||||
| #include <libavresample/avresample.h> | ||||
| #include <libavutil/opt.h> | ||||
| #include <string.h> | ||||
| #include <libswscale/swscale.h> | ||||
|  | ||||
| typedef struct { | ||||
| 	AVCodec *codec; | ||||
| 	AVCodecContext *codecCtx; | ||||
| 	AVFrame *frame; | ||||
| 	AVDictionary *options; | ||||
| 	int profile; | ||||
| } FFCtx; | ||||
|  | ||||
| static inline int avcodec_profile_name_to_int(AVCodec *codec, const char *name) { | ||||
| 	const AVProfile *p; | ||||
| 	for (p = codec->profiles; p != NULL && p->profile != FF_PROFILE_UNKNOWN; p++) | ||||
| 		if (!strcasecmp(p->name, name)) | ||||
| 			return p->profile; | ||||
| 	return FF_PROFILE_UNKNOWN; | ||||
| } | ||||
|  | ||||
							
								
								
									
										124
									
								
								cgo/ffmpeg/video.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								cgo/ffmpeg/video.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | ||||
| package ffmpeg | ||||
|  | ||||
| /* | ||||
| #include "ffmpeg.h" | ||||
| int wrap_avcodec_decode_video2(AVCodecContext *ctx, AVFrame *frame, void *data, int size, int *got) { | ||||
| 	struct AVPacket pkt = {.data = data, .size = size}; | ||||
| 	return avcodec_decode_video2(ctx, frame, got, &pkt); | ||||
| } | ||||
| */ | ||||
| import "C" | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"image" | ||||
| 	"reflect" | ||||
| 	"runtime" | ||||
| 	"unsafe" | ||||
|  | ||||
| 	"github.com/deepch/vdk/av" | ||||
| 	"github.com/deepch/vdk/codec/h264parser" | ||||
| ) | ||||
|  | ||||
| type VideoDecoder struct { | ||||
| 	ff        *ffctx | ||||
| 	Extradata []byte | ||||
| } | ||||
|  | ||||
| func (self *VideoDecoder) Setup() (err error) { | ||||
| 	ff := &self.ff.ff | ||||
| 	if len(self.Extradata) > 0 { | ||||
| 		ff.codecCtx.extradata = (*C.uint8_t)(unsafe.Pointer(&self.Extradata[0])) | ||||
| 		ff.codecCtx.extradata_size = C.int(len(self.Extradata)) | ||||
| 	} | ||||
| 	if C.avcodec_open2(ff.codecCtx, ff.codec, nil) != 0 { | ||||
| 		err = fmt.Errorf("ffmpeg: decoder: avcodec_open2 failed") | ||||
| 		return | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func fromCPtr(buf unsafe.Pointer, size int) (ret []uint8) { | ||||
| 	hdr := (*reflect.SliceHeader)((unsafe.Pointer(&ret))) | ||||
| 	hdr.Cap = size | ||||
| 	hdr.Len = size | ||||
| 	hdr.Data = uintptr(buf) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| type VideoFrame struct { | ||||
| 	Image image.YCbCr | ||||
| 	frame *C.AVFrame | ||||
| } | ||||
|  | ||||
| func (self *VideoFrame) Free() { | ||||
| 	self.Image = image.YCbCr{} | ||||
| 	C.av_frame_free(&self.frame) | ||||
| } | ||||
|  | ||||
| func freeVideoFrame(self *VideoFrame) { | ||||
| 	self.Free() | ||||
| } | ||||
|  | ||||
| func (self *VideoDecoder) Decode(pkt []byte) (img *VideoFrame, err error) { | ||||
| 	ff := &self.ff.ff | ||||
|  | ||||
| 	cgotimg := C.int(0) | ||||
| 	frame := C.av_frame_alloc() | ||||
| 	cerr := C.wrap_avcodec_decode_video2(ff.codecCtx, frame, unsafe.Pointer(&pkt[0]), C.int(len(pkt)), &cgotimg) | ||||
| 	if cerr < C.int(0) { | ||||
| 		err = fmt.Errorf("ffmpeg: avcodec_decode_video2 failed: %d", cerr) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if cgotimg != C.int(0) { | ||||
| 		w := int(frame.width) | ||||
| 		h := int(frame.height) | ||||
| 		ys := int(frame.linesize[0]) | ||||
| 		cs := int(frame.linesize[1]) | ||||
|  | ||||
| 		img = &VideoFrame{Image: image.YCbCr{ | ||||
| 			Y:              fromCPtr(unsafe.Pointer(frame.data[0]), ys*h), | ||||
| 			Cb:             fromCPtr(unsafe.Pointer(frame.data[1]), cs*h/2), | ||||
| 			Cr:             fromCPtr(unsafe.Pointer(frame.data[2]), cs*h/2), | ||||
| 			YStride:        ys, | ||||
| 			CStride:        cs, | ||||
| 			SubsampleRatio: image.YCbCrSubsampleRatio420, | ||||
| 			Rect:           image.Rect(0, 0, w, h), | ||||
| 		}, frame: frame} | ||||
| 		runtime.SetFinalizer(img, freeVideoFrame) | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func NewVideoDecoder(stream av.CodecData) (dec *VideoDecoder, err error) { | ||||
| 	_dec := &VideoDecoder{} | ||||
| 	var id uint32 | ||||
|  | ||||
| 	switch stream.Type() { | ||||
| 	case av.H264: | ||||
| 		h264 := stream.(h264parser.CodecData) | ||||
| 		_dec.Extradata = h264.AVCDecoderConfRecordBytes() | ||||
| 		id = C.AV_CODEC_ID_H264 | ||||
|  | ||||
| 	default: | ||||
| 		err = fmt.Errorf("ffmpeg: NewVideoDecoder codec=%v unsupported", stream.Type()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	c := C.avcodec_find_decoder(id) | ||||
| 	if c == nil || C.avcodec_get_type(id) != C.AVMEDIA_TYPE_VIDEO { | ||||
| 		err = fmt.Errorf("ffmpeg: cannot find video decoder codecId=%d", id) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if _dec.ff, err = newFFCtxByCodec(c); err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	if err = _dec.Setup(); err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	dec = _dec | ||||
| 	return | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Andrey Semochkin
					Andrey Semochkin