/* 
 *  Squeezelite - lightweight headless squeezebox emulator
 *
 *  (c) Adrian Smith 2012-2015, triode1@btinternet.com
 *      Ralph Irving 2015-2024, ralph_irving@hotmail.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "squeezelite.h"

#if FFMPEG

#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>

#define READ_SIZE  4096 * 4   // this is large enough to ensure ffmpeg always gets new data when decode is called
#define WRITE_SIZE 256 * 1024 // FIXME - make smaller, but still to absorb max wma output

// FIXME - do we need to align these params as per ffmpeg on i386? 
#define attribute_align_arg

struct ff_s {
	// state for ffmpeg decoder
	bool wma;
	u8_t wma_mmsh;
	u8_t wma_playstream;
	u8_t wma_metadatastream;
	u8_t *readbuf;
	bool end_of_stream;
	AVInputFormat *input_format;
	AVFormatContext *formatC;
	AVCodecContext *codecC;
	AVCodecParameters *codecP;
	AVFrame *frame;
	AVPacket *avpkt;
	unsigned mmsh_bytes_left;
	unsigned mmsh_bytes_pad;
	unsigned mmsh_packet_len;
#if !LINKALL
	// ffmpeg symbols to be dynamically loaded from libavcodec
	unsigned (* avcodec_version)(void);
	AVCodec * (* avcodec_find_decoder)(int);
	int attribute_align_arg (* avcodec_open2)(AVCodecContext *, const AVCodec *, AVDictionary **);
	AVFrame * (* av_frame_alloc)(void);
	void (* av_frame_free)(AVFrame **);
	int attribute_align_arg (* avcodec_send_packet)(AVCodecContext *, const AVPacket *);
	int attribute_align_arg (* avcodec_receive_frame)(AVCodecContext *, AVFrame *);
	AVCodecContext * (* avcodec_alloc_context3)(const AVCodec *);
	void (* avcodec_free_context)(AVCodecContext **);
	int (* avcodec_parameters_to_context)(AVCodecContext *, const AVCodecParameters *);
	// ffmpeg symbols to be dynamically loaded from libavformat
	unsigned (* avformat_version)(void);
	AVFormatContext * (* avformat_alloc_context)(void);
	void (* avformat_free_context)(AVFormatContext *);
	int (* avformat_open_input)(AVFormatContext **, const char *, AVInputFormat *, AVDictionary **);
	int (* avformat_find_stream_info)(AVFormatContext *, AVDictionary **);
	AVIOContext * (* avio_alloc_context)(unsigned char *, int, int,	void *,
		int (*read_packet)(void *, uint8_t *, int), int (*write_packet)(void *, uint8_t *, int), int64_t (*seek)(void *, int64_t, int));
	AVPacket * (* av_packet_alloc)(void);
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58,133,100)
	void (* av_init_packet)(AVPacket *);
#endif
	void (* av_packet_unref)(AVPacket *);
	int (* av_read_frame)(AVFormatContext *, AVPacket *);
	AVInputFormat * (* av_find_input_format)(const char *);
#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58,9,100)
	void (* av_register_all)(void);
#endif
	// ffmpeg symbols to be dynamically loaded from libavutil
	unsigned (* avutil_version)(void);
	void (* av_log_set_callback)(void (*)(void*, int, const char*, va_list));
	void (* av_log_set_level)(int);
	int  (* av_strerror)(int, char *, size_t);
	void * (* av_malloc)(size_t);
	void (* av_freep)(void *);
#endif
};

static struct ff_s *ff;

extern log_level loglevel;

extern struct buffer *streambuf;
extern struct buffer *outputbuf;
extern struct streamstate stream;
extern struct outputstate output;
extern struct decodestate decode;
extern struct processstate process;

#define LOCK_S   mutex_lock(streambuf->mutex)
#define UNLOCK_S mutex_unlock(streambuf->mutex)
#define LOCK_O   mutex_lock(outputbuf->mutex)
#define UNLOCK_O mutex_unlock(outputbuf->mutex)
#if PROCESS
#define LOCK_O_direct   if (decode.direct) mutex_lock(outputbuf->mutex)
#define UNLOCK_O_direct if (decode.direct) mutex_unlock(outputbuf->mutex)
#define IF_DIRECT(x)    if (decode.direct) { x }
#define IF_PROCESS(x)   if (!decode.direct) { x }
#else
#define LOCK_O_direct   mutex_lock(outputbuf->mutex)
#define UNLOCK_O_direct mutex_unlock(outputbuf->mutex)
#define IF_DIRECT(x)    { x }
#define IF_PROCESS(x)
#endif

#if LINKALL
#define AV(h, fn, ...)       (av_ ## fn)(__VA_ARGS__)
#define AVIO(h, fn, ...)     (avio_ ## fn)(__VA_ARGS__)
#define AVCODEC(h, fn, ...)  (avcodec_ ## fn)(__VA_ARGS__)
#define AVFORMAT(h, fn, ...) (avformat_ ## fn)(__VA_ARGS__)
#else
#define AV(h, fn, ...)       (h)->av_##fn(__VA_ARGS__)
#define AVIO(h, fn, ...)     (h)->avio_##fn(__VA_ARGS__)
#define AVCODEC(h, fn, ...)  (h)->avcodec_##fn(__VA_ARGS__)
#define AVFORMAT(h, fn, ...) (h)->avformat_##fn(__VA_ARGS__)
#endif


// our own version of useful error function not included in earlier ffmpeg versions
static char *av__err2str(int errnum) {
	static char buf[64];
	AV(ff, strerror, errnum, buf, 64); 
	return buf;
}

// parser to extract asf data packet length from asf header
const u8_t header_guid[16] = { 0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11, 0xA6, 0xD9, 0x00, 0xAA, 0x00, 0x62, 0xCE, 0x6C };
const u8_t file_props_guid[16] = { 0xA1, 0xDC, 0xAB, 0x8C, 0x47, 0xA9, 0xCF, 0x11, 0x8E, 0xE4, 0x00, 0xC0, 0x0C, 0x20, 0x53, 0x65 };

static int _parse_packlen(void) {
	int bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf));
	u8_t *ptr = streambuf->readp;
	int remain = 1;

	while (bytes >= 24 && remain > 0) {
		u32_t len = *(ptr+16) | *(ptr+17) << 8 | *(ptr+18) << 16 | *(ptr+19) << 24; // assume msb 32 bits are 0
		if (!memcmp(ptr, header_guid, 16) && bytes >= 30) {
			ptr    += 30;
			bytes  -= 30;
			remain = len - 30;
			continue;
		}
		if (!memcmp(ptr, file_props_guid, 16) && len == 104) {
			u32_t packlen = *(ptr+92) | *(ptr+93) << 8 | *(ptr+94) << 16 | *(ptr+95) << 24;
			LOG_INFO("asf packet len: %u", packlen);
			return packlen;
		}
		ptr    += len;
		bytes  -= len;
		remain -= len;
	}

	LOG_WARN("could not parse packet length");
	return 0;
}

static int _read_data(void *opaque, u8_t *buffer, int buf_size) {
	unsigned int bytes;

	LOCK_S;

	bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf));
	ff->end_of_stream = (stream.state <= DISCONNECT && bytes == 0);
	bytes = min(bytes, buf_size);

	// for chunked wma extract asf header and data frames from framing structure
	// pad asf data frames to size of packet extracted from asf header
	if (ff->wma_mmsh) {
		unsigned chunk_type = 0, chunk_len = 0;
		
		if (ff->mmsh_bytes_left) {
			// bytes remaining from previous frame
			if (bytes >= ff->mmsh_bytes_left) {
				bytes = ff->mmsh_bytes_left;
				ff->mmsh_bytes_left = 0;
			} else {
				ff->mmsh_bytes_left -= bytes;
			}
		} else if (ff->mmsh_bytes_pad) {
			// add padding for previous frame
			bytes = min(ff->mmsh_bytes_pad, buf_size);
			memset(buffer, 0, bytes);
			ff->mmsh_bytes_pad -= bytes;
			UNLOCK_S;
			return bytes;
		} else if (bytes >= 12) {
			// new chunk header
			chunk_type = (*(streambuf->readp) & 0x7f) | *(streambuf->readp + 1) << 8;
			chunk_len = *(streambuf->readp + 2) | *(streambuf->readp + 3) << 8;
			_buf_inc_readp(streambuf, 12);
			bytes -= 12;
		} else if (_buf_used(streambuf) >= 12) {
			// new chunk header split over end of streambuf, read in two
			u8_t header[12];
			memcpy(header, streambuf->readp, bytes);
			_buf_inc_readp(streambuf, bytes);
			memcpy(header + bytes, streambuf->readp, 12 - bytes);
			_buf_inc_readp(streambuf, 12 - bytes);
			chunk_type = (header[0] & 0x7f) | header[1] << 8;
			chunk_len  = header[2] | header[3] << 8;
			bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf));
			bytes = min(bytes, buf_size);
		} else {
			// should not get here...
			LOG_ERROR("chunk parser stalled bytes: %u %u", bytes, _buf_used(streambuf));
			UNLOCK_S;
			return 0;
		}
		
		if (chunk_type && chunk_len) {
			if (chunk_type == 0x4824) {
				// asf header - parse packet length
				ff->mmsh_packet_len = _parse_packlen();
				ff->mmsh_bytes_pad = 0;
			} else if (chunk_type == 0x4424 && ff->mmsh_packet_len) {
				// asf data packet - add padding
				ff->mmsh_bytes_pad = ff->mmsh_packet_len - chunk_len + 8;
			} else {
				LOG_INFO("unknown chunk: %04x", chunk_type);
				// other packet - no padding
				ff->mmsh_bytes_pad = 0;
			}
	
			if (chunk_len - 8 <= bytes) {
				bytes = chunk_len - 8;
				ff->mmsh_bytes_left = 0;
			} else {
				ff->mmsh_bytes_left = chunk_len - 8 - bytes;
			}
		}

	}

	memcpy(buffer, streambuf->readp, bytes);

	_buf_inc_readp(streambuf, bytes);

	if (ff->mmsh_bytes_pad && bytes + ff->mmsh_bytes_pad < buf_size) {
		memset(buffer + bytes, 0, ff->mmsh_bytes_pad);
		bytes += ff->mmsh_bytes_pad;
		ff->mmsh_bytes_pad = 0;
	}

	UNLOCK_S;

	return bytes;
}

static decode_state ff_decode(void) {
	int r;
	s32_t *optr = NULL;

	if (decode.new_stream) {

		AVIOContext *avio;
		AVStream *av_stream;
		AVCodec *codec;
		int o;
		int audio_stream = -1;

		ff->mmsh_bytes_left = ff->mmsh_bytes_pad = ff->mmsh_packet_len = 0;

		if (!ff->readbuf) {
			ff->readbuf = AV(ff, malloc, READ_SIZE +  AV_INPUT_BUFFER_PADDING_SIZE);
		}

		avio = AVIO(ff, alloc_context, ff->readbuf, READ_SIZE, 0, NULL, _read_data, NULL, NULL);
		avio->seekable = 0;

		ff->formatC = AVFORMAT(ff, alloc_context);
		if (ff->formatC == NULL) {
			LOG_ERROR("null context");
			return DECODE_ERROR;
		}

		ff->formatC->pb = avio;
		ff->formatC->flags |= AVFMT_FLAG_CUSTOM_IO | AVFMT_FLAG_NOPARSE;

		o = AVFORMAT(ff, open_input, &ff->formatC, "", ff->input_format, NULL);
		if (o < 0) {
			LOG_WARN("avformat_open_input: %d %s", o, av__err2str(o));
			return DECODE_ERROR;
		}

		LOG_INFO("format: name:%s lname:%s", ff->formatC->iformat->name, ff->formatC->iformat->long_name);
	
		o = AVFORMAT(ff, find_stream_info, ff->formatC, NULL);
		if (o < 0) {
			LOG_WARN("avformat_find_stream_info: %d %s", o, av__err2str(o));
			return DECODE_ERROR;
		}
		
		if (ff->wma && ff->wma_playstream < ff->formatC->nb_streams) {
			if (ff->formatC->streams[ff->wma_playstream]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
				LOG_INFO("using wma stream sent from server: %i", ff->wma_playstream);
				audio_stream = ff->wma_playstream;
			}
		}

		if (audio_stream == -1) {
			unsigned int i;
			for (i = 0; i < ff->formatC->nb_streams; ++i) {
				if (ff->formatC->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
					audio_stream = i;
					LOG_INFO("found stream: %i", i);
					break;
				}
			}
		}

		if (audio_stream == -1) {
			LOG_WARN("no audio stream found");
			return DECODE_ERROR;
		}

		av_stream = ff->formatC->streams[audio_stream];

		ff->codecC = AVCODEC (ff, alloc_context3, NULL);
		if ( ff->codecC == NULL ) {
			LOG_ERROR("can't allocate avctx");
			return DECODE_ERROR;
		}

		ff->codecP = av_stream->codecpar;

		if ( (AVCODEC(ff, parameters_to_context, ff->codecC, ff->codecP) ) < 0) {
			AVCODEC(ff, free_context, &ff->codecC);

			LOG_ERROR("can't initialize avctx");
			return DECODE_ERROR;
		}

		codec = AVCODEC(ff, find_decoder, ff->codecP->codec_id);

		AVCODEC(ff, open2, ff->codecC, codec, NULL);

		ff->frame = AV(ff, frame_alloc);
		ff->avpkt = AV(ff, packet_alloc);
		if (ff->avpkt == NULL) {
			LOG_ERROR("can't allocate avpkt");
			return DECODE_ERROR;
		}

#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58,133,100)
		AV(ff, init_packet, ff->avpkt); // on older ffmpeg, this still is needed to e.g. set ->pos to -1
#endif

		LOCK_O;
		LOG_INFO("setting track_start");
		output.next_sample_rate = decode_newstream(ff->codecP->sample_rate, output.supported_rates);
		IF_DSD(	output.next_fmt = PCM; )
		output.track_start = outputbuf->writep;
		if (output.fade_mode) _checkfade(true);
		decode.new_stream = false;
		UNLOCK_O;
	}

	if ((r = AV(ff, read_frame, ff->formatC, ff->avpkt)) < 0) {
		if (r == AVERROR_EOF) {
			if (ff->end_of_stream) {
				LOG_INFO("decode complete");
				return DECODE_COMPLETE;
			} else {
				LOG_INFO("codec end of file");
			}
		} else {
			LOG_ERROR("av_read_frame error: %i %s", r, av__err2str(r));
		}
		return DECODE_RUNNING;
	}

	IF_PROCESS(
		optr = (s32_t *)process.inbuf;
		process.in_frames = 0;
	);

	if ((r = AVCODEC(ff, send_packet, ff->codecC, ff->avpkt)) < 0) {
		AV(ff, packet_unref, ff->avpkt);

		if (r == AVERROR_EOF) {
			LOG_DEBUG("av_send_packet reports eof");
			return DECODE_COMPLETE;
		} else {
			LOG_ERROR("av_send_packet error: %i %s", r, av__err2str(r));

			// EAGAIN is treated as an unrecoverable error here since all pending output frames are processed below
			if (r == AVERROR(EAGAIN) || r == AVERROR(EINVAL) || r == AVERROR(ENOMEM))
				return DECODE_ERROR;

			// retain existing behavior (continue decoding) for legitimate decoding errors
			return DECODE_RUNNING;
		}
	} else {
		// a single input packet can lead to multiple output frames - process all of them
		while ((r = AVCODEC(ff, receive_frame, ff->codecC, ff->frame)) >= 0) {
			s16_t *iptr16 = (s16_t *)ff->frame->data[0];
			s32_t *iptr32 = (s32_t *)ff->frame->data[0];
			s16_t *iptr16l = (s16_t *)ff->frame->data[0];
			s16_t *iptr16r = (s16_t *)ff->frame->data[1];
			s32_t *iptr32l = (s32_t *)ff->frame->data[0];
			s32_t *iptr32r = (s32_t *)ff->frame->data[1];
			float *iptrfl = (float *)ff->frame->data[0];
			float *iptrfr = (float *)ff->frame->data[1];

			frames_t frames = ff->frame->nb_samples;

			LOG_SDEBUG("got audio channels: %u samples: %u format: %u", ff->codecC->channels, ff->frame->nb_samples,
					   ff->codecC->sample_fmt);
			
			LOCK_O_direct;

			while (frames > 0) {
				frames_t count;
				frames_t f;
				
				IF_DIRECT(
					optr = (s32_t *)outputbuf->writep;
					f = min(_buf_space(outputbuf), _buf_cont_write(outputbuf)) / BYTES_PER_FRAME;
					f = min(f, frames);
				);

				IF_PROCESS(
					if (process.in_frames + frames > process.max_in_frames) {
						LOG_WARN("exceeded process buffer size - dropping frames");
						break;
					}
					f = frames;	   
				);

				count = f;
				
				if (ff->codecC->channels == 2) {
					if (ff->codecC->sample_fmt == AV_SAMPLE_FMT_S16) {
						while (count--) {
							*optr++ = *iptr16++ << 16;
							*optr++ = *iptr16++ << 16;
						}
					} else if (ff->codecC->sample_fmt == AV_SAMPLE_FMT_S32) {
						while (count--) {
							*optr++ = *iptr32++;
							*optr++ = *iptr32++;
						}
					} else if (ff->codecC->sample_fmt == AV_SAMPLE_FMT_S16P) {
						while (count--) {
							*optr++ = *iptr16l++ << 16;
							*optr++ = *iptr16r++ << 16;
						}
					} else if (ff->codecC->sample_fmt == AV_SAMPLE_FMT_S32P) {
						while (count--) {
							*optr++ = *iptr32l++;
							*optr++ = *iptr32r++;
						}
					} else if (ff->codecC->sample_fmt == AV_SAMPLE_FMT_FLTP) {
						while (count--) {
							double scaledl = *iptrfl++ * 0x7fffffff;
							double scaledr = *iptrfr++ * 0x7fffffff;
							if (scaledl > 2147483647.0) scaledl = 2147483647.0;
							if (scaledl < -2147483648.0) scaledl = -2147483648.0;
							if (scaledr > 2147483647.0) scaledr = 2147483647.0;
							if (scaledr < -2147483648.0) scaledr = -2147483648.0;
							*optr++ = (s32_t)scaledl;
							*optr++ = (s32_t)scaledr;
						}
					} else {
						LOG_WARN("unsupported sample format: %u", ff->codecC->sample_fmt);
					}
				} else if (ff->codecC->channels == 1) {
					if (ff->codecC->sample_fmt == AV_SAMPLE_FMT_S16) {
						while (count--) {
							*optr++ = *iptr16   << 16;
							*optr++ = *iptr16++ << 16;
						}
					} else if (ff->codecC->sample_fmt == AV_SAMPLE_FMT_S32) {
						while (count--) {
							*optr++ = *iptr32;
							*optr++ = *iptr32++;						
						}
					} else if (ff->codecC->sample_fmt == AV_SAMPLE_FMT_S16P) {
						while (count--) {
							*optr++ = *iptr16l << 16;
							*optr++ = *iptr16l++ << 16;
						}
					} else if (ff->codecC->sample_fmt == AV_SAMPLE_FMT_S32P) {
						while (count--) {
							*optr++ = *iptr32l;
							*optr++ = *iptr32l++;
						}
					} else if (ff->codecC->sample_fmt == AV_SAMPLE_FMT_FLTP) {
						while (count--) {
							double scaled = *iptrfl++ * 0x7fffffff;
							if (scaled > 2147483647.0) scaled = 2147483647.0;
							if (scaled < -2147483648.0) scaled = -2147483648.0;
							*optr++ = (s32_t)scaled;
							*optr++ = (s32_t)scaled;
						}
					} else {
						LOG_WARN("unsupported sample format: %u", ff->codecC->sample_fmt);
					}
				} else {
					LOG_WARN("unsupported number of channels");
				}
				
				frames -= f;
				
				IF_DIRECT(
					_buf_inc_writep(outputbuf, f * BYTES_PER_FRAME);
				);

				IF_PROCESS(
					process.in_frames += f;
				);
			}
			
			UNLOCK_O_direct;
		}
	}

	AV(ff, packet_unref, ff->avpkt);

	if (r == AVERROR(EAGAIN)) {
		// EAGAIN is expected if there are no more pending frames
	} else if (r == AVERROR_EOF) {
		LOG_DEBUG("av_receive_frame reports eof");
		return DECODE_COMPLETE;
	} else {
		LOG_ERROR("av_receive_frame error: %i %s", r, av__err2str(r));

		if (r == AVERROR(EINVAL))
			return DECODE_ERROR;

		// retain existing behavior (continue decoding) for legitimate decoding errors
	}

	return DECODE_RUNNING;
}

static void _free_ff_data(void) {
	if (ff->codecC) {
		AVCODEC(ff, free_context, &ff->codecC);
		ff->codecC = NULL;
	}

	if (ff->formatC) {
		if (ff->formatC->pb) {
			// per ffmpeg docs, the buffer originally pointed to by ff->readbuf may be dynamically freed and reallocated behind the scenes, so this is the one that must be freed
			// otherwise, a double free can occur (seen by valgrind), resulting in e.g. SIGILL
			AV(ff, freep, &ff->formatC->pb->buffer);
			ff->readbuf = NULL;
			AV(ff, freep, &ff->formatC->pb);
		}

		AVFORMAT(ff, free_context, ff->formatC);
		ff->formatC = NULL;
	}

	if (ff->frame) {
		AV(ff, frame_free, &ff->frame);
		ff->frame = NULL;
	}

	if (ff->avpkt) {
		AV(ff, packet_unref, ff->avpkt);
		AV(ff, freep, &ff->avpkt);
		ff->avpkt = NULL;
	}
}

static void ff_open_wma(u8_t size, u8_t rate, u8_t chan, u8_t endianness) {
	_free_ff_data();

	ff->input_format = AV(ff, find_input_format, "asf");
	if (ff->input_format == NULL) {
		LOG_ERROR("asf format not supported by ffmpeg library");
	}

	ff->wma = true;
	ff->wma_mmsh = size - '0';
	ff->wma_playstream = rate - 1;
	ff->wma_metadatastream = chan != '?' ? chan : 0;

	LOG_INFO("open wma chunking: %u playstream: %u metadatastream: %u", ff->wma_mmsh, ff->wma_playstream, ff->wma_metadatastream);
}

static void ff_open_alac(u8_t size, u8_t rate, u8_t chan, u8_t endianness) {
	_free_ff_data();

	ff->input_format = AV(ff, find_input_format, "mp4");
	if (ff->input_format == NULL) {
		LOG_ERROR("mp4 format not supported by ffmpeg library");
	}

	ff->wma = false;
	ff->wma_mmsh = 0;

	LOG_INFO("open alac");
}

static void ff_close(void) {
	_free_ff_data();
}

static bool load_ff() {
#if !LINKALL
	void *handle_codec = NULL, *handle_format = NULL, *handle_util = NULL;
	char name[30];
	char *err;

	// we try to load the ffmpeg library version which matches the header file we are compiled with as structs differ between versions

	sprintf(name, LIBAVCODEC, LIBAVCODEC_VERSION_MAJOR);
	handle_codec = dlopen(name, RTLD_NOW);
	if (!handle_codec) {
		LOG_INFO("dlerror: %s", dlerror());
		return false;
	}

	sprintf(name, LIBAVFORMAT, LIBAVFORMAT_VERSION_MAJOR);
	handle_format = dlopen(name, RTLD_NOW);
	if (!handle_format) {
		LOG_INFO("dlerror: %s", dlerror());
		return false;
	}

	sprintf(name, LIBAVUTIL, LIBAVUTIL_VERSION_MAJOR);
	handle_util = dlopen(name, RTLD_NOW);
	if (!handle_util) {
		LOG_INFO("dlerror: %s", dlerror());
		return false;
	}

	ff->avcodec_version = dlsym(handle_codec, "avcodec_version");
	ff->avcodec_find_decoder = dlsym(handle_codec, "avcodec_find_decoder");
	ff->avcodec_open2 = dlsym(handle_codec, "avcodec_open2");
	ff->av_frame_alloc = dlsym(handle_codec, "av_frame_alloc");
	ff->av_frame_free = dlsym(handle_codec, "av_frame_free");
	ff->avcodec_send_packet = dlsym(handle_codec, "avcodec_send_packet");
	ff->avcodec_receive_frame = dlsym(handle_codec, "avcodec_receive_frame");
	ff->avcodec_alloc_context3 = dlsym(handle_format, "avcodec_alloc_context3");
	ff->avcodec_free_context = dlsym(handle_format, "avcodec_free_context");
	ff->avcodec_parameters_to_context = dlsym(handle_format, "avcodec_parameters_to_context");
	ff->av_packet_alloc = dlsym(handle_codec, "av_packet_alloc");
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58,133,100)
	ff->av_init_packet = dlsym(handle_codec, "av_init_packet");
#endif
	ff->av_packet_unref = dlsym(handle_codec, "av_packet_unref");

	if ((err = dlerror()) != NULL) {
		LOG_INFO("dlerror: %s", err);		
		return false;
	}
	
	LOG_INFO("loaded "LIBAVCODEC" (%u.%u.%u)", LIBAVCODEC_VERSION_MAJOR, ff->avcodec_version() >> 16, (ff->avcodec_version() >> 8) & 0xff, ff->avcodec_version() & 0xff);

	ff->avformat_version = dlsym(handle_format, "avformat_version");
	ff->avformat_alloc_context = dlsym(handle_format, "avformat_alloc_context");
	ff->avformat_free_context = dlsym(handle_format, "avformat_free_context");
	ff->avformat_open_input = dlsym(handle_format, "avformat_open_input");
	ff->avformat_find_stream_info = dlsym(handle_format, "avformat_find_stream_info");
	ff->avio_alloc_context = dlsym(handle_format, "avio_alloc_context");
	ff->av_read_frame = dlsym(handle_format, "av_read_frame");
	ff->av_find_input_format= dlsym(handle_format, "av_find_input_format");
#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58,9,100)
	ff->av_register_all = dlsym(handle_format, "av_register_all");
#endif

	if ((err = dlerror()) != NULL) {
		LOG_INFO("dlerror: %s", err);		
		return false;
	}

	LOG_INFO("loaded "LIBAVFORMAT" (%u.%u.%u)", LIBAVFORMAT_VERSION_MAJOR, ff->avformat_version() >> 16, (ff->avformat_version() >> 8) & 0xff, ff->avformat_version() & 0xff);

	ff->avutil_version = dlsym(handle_util, "avutil_version");
	ff->av_log_set_callback = dlsym(handle_util, "av_log_set_callback");
	ff->av_log_set_level = dlsym(handle_util, "av_log_set_level");
	ff->av_strerror = dlsym(handle_util, "av_strerror");
	ff->av_malloc = dlsym(handle_util, "av_malloc");
	ff->av_freep = dlsym(handle_util, "av_freep");

	if ((err = dlerror()) != NULL) {
		LOG_INFO("dlerror: %s", err);		
		return false;
	}

	LOG_INFO("loaded "LIBAVUTIL" (%u.%u.%u)", LIBAVUTIL_VERSION_MAJOR, ff->avutil_version() >> 16, (ff->avutil_version() >> 8) & 0xff, ff->avutil_version() & 0xff);

#endif

	return true;
}

static int ff_log_level = 0;

void av_err_callback(void *avcl, int level, const char *fmt, va_list vl) {
	if (level > ff_log_level) return;
	fprintf(stderr, "%s ffmpeg: ", logtime());
	vfprintf(stderr, fmt, vl);
	fflush(stderr);
}

static bool registered = false;

struct codec *register_ff(const char *codec) {
	if (!registered) {

		ff = malloc(sizeof(struct ff_s));
		if (!ff) {
			return NULL;
		}

		memset(ff, 0, sizeof(struct ff_s));

		if (!load_ff()) {
			return NULL;
		}

		switch (loglevel) {
		case lERROR:
			ff_log_level = AV_LOG_ERROR; break;
		case lWARN:
			ff_log_level = AV_LOG_WARNING; break;
		case lINFO:
			ff_log_level = AV_LOG_INFO; break;
		case lDEBUG:
			ff_log_level = AV_LOG_VERBOSE; break;
		default: break;
		}

		AV(ff, log_set_callback, av_err_callback);

#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58,9,100)
		AV(ff, register_all);
#endif

		registered = true;
	}

	if (!strcmp(codec, "wma")) {

		static struct codec ret = { 
			'w',         // id
			"wma,wmap,wmal", // types
			READ_SIZE,   // min read
			WRITE_SIZE,  // min space
			ff_open_wma, // open
			ff_close,    // close
			ff_decode,   // decode
		};
		
		LOG_INFO("using ffmpeg to decode wma,wmap,wmal");
		return &ret;
	}

	if (!strcmp(codec, "alc")) {

		static struct codec ret = { 
			'l',         // id
			"alc",       // types
			READ_SIZE,   // min read
			WRITE_SIZE,  // min space
			ff_open_alac,// open
			ff_close,    // close
			ff_decode,   // decode
		};
		
		LOG_INFO("using ffmpeg to decode alc");		
		return &ret;
	}

	return NULL;
}

#endif
