unit UAudioDecoder_FFMpeg;
(*******************************************************************************
This unit is primarily based upon -
http://www.dranger.com/ffmpeg/ffmpegtutorial_all.html
and tutorial03.c
http://www.inb.uni-luebeck.de/~boehme/using_libavcodec.html
*******************************************************************************)
interface
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
{$I switches.inc}
//{$DEFINE DebugFFMpegDecode}
uses
Classes,
SysUtils,
UMusic;
implementation
uses
UIni,
UMain,
avcodec, // FFMpeg Audio file decoding
avformat,
avutil,
avio, // used for url_ferror
mathematics, // used for av_rescale_q
rational,
SDL,
ULog,
UCommon,
UConfig;
type
PPacketQueue = ^TPacketQueue;
TPacketQueue = class
private
firstPkt,
lastPkt : PAVPacketList;
nbPackets : integer;
size : integer;
mutex : PSDL_Mutex;
cond : PSDL_Cond;
abortRequest: boolean;
public
constructor Create();
destructor Destroy(); override;
function Put(pkt : PAVPacket): integer;
function PutStatus(statusFlag: integer; statusInfo: Pointer): integer;
function Get(var pkt: TAVPacket; block: boolean): integer;
procedure Flush();
procedure Abort();
end;
const
MAX_AUDIOQ_SIZE = (5 * 16 * 1024);
const
STATUS_PACKET: PChar = 'STATUS_PACKET';
const
PKT_STATUS_FLAG_EOF = 1;
PKT_STATUS_FLAG_FLUSH = 2;
PKT_STATUS_FLAG_ERROR = 3;
type
PAudioBuffer = ^TAudioBuffer;
// TODO: should (or must?) be aligned at a 2-byte boundary.
// ffmpeg provides a C-macro called DECLARE_ALIGNED for this.
// Records can be aligned with the $PACKRECORDS compiler-directive but
// are already aligned at a 2-byte boundary by default in FPC.
// But what about arrays, are they aligned by a 2-byte boundary too?
// Or maybe we have to define a fake record with only an array in it?
TAudioBuffer = array[0 .. (AVCODEC_MAX_AUDIO_FRAME_SIZE * 3 div 2)-1] of byte;
type
TFFMpegDecodeStream = class(TAudioDecodeStream)
private
decoderLock : PSDL_Mutex;
parserLock : PSDL_Mutex;
myint: integer;
EOFState: boolean; // end-of-stream flag
ErrorState: boolean;
resumeCond : PSDL_Cond;
quitRequest : boolean;
seekRequest: boolean;
seekFlags : integer;
seekPos : int64;
parseThread: PSDL_Thread;
packetQueue: TPacketQueue;
formatInfo : TAudioFormatInfo;
// FFMpeg internal data
pFormatCtx : PAVFormatContext;
pCodecCtx : PAVCodecContext;
pCodec : PAVCodec;
ffmpegStreamIndex : Integer;
ffmpegStream : PAVStream;
audioClock: double; // stream position in seconds
// state-vars for DecodeFrame
pkt : TAVPacket;
audio_pkt_data : PChar;
audio_pkt_size : integer;
// state-vars for AudioCallback
audio_buf_index : integer;
audio_buf_size : integer;
audio_buf : TAudioBuffer;
procedure LockParser(); {$IFDEF HasInline}inline;{$ENDIF}
procedure UnlockParser(); {$IFDEF HasInline}inline;{$ENDIF}
function GetParserMutex(): PSDL_Mutex; {$IFDEF HasInline}inline;{$ENDIF}
procedure LockDecoder(); {$IFDEF HasInline}inline;{$ENDIF}
procedure UnlockDecoder(); {$IFDEF HasInline}inline;{$ENDIF}
procedure SetEOF(state: boolean); {$IFDEF HasInline}inline;{$ENDIF}
procedure SetError(state: boolean); {$IFDEF HasInline}inline;{$ENDIF}
procedure ParseAudio();
function DecodeFrame(var buffer: TAudioBuffer; bufSize: integer): integer;
public
constructor Create(pFormatCtx: PAVFormatContext;
pCodecCtx: PAVCodecContext; pCodec: PAVCodec;
ffmpegStreamIndex: Integer; ffmpegStream: PAVStream);
destructor Destroy(); override;
procedure Close(); override;
function GetLength(): real; override;
function GetAudioFormatInfo(): TAudioFormatInfo; override;
function GetPosition: real; override;
procedure SetPosition(Time: real); override;
function IsEOF(): boolean; override;
function IsError(): boolean; override;
function ReadData(Buffer: PChar; BufSize: integer): integer; override;
end;
type
TAudioDecoder_FFMpeg = class( TInterfacedObject, IAudioDecoder )
private
class function FindAudioStreamIndex(pFormatCtx : PAVFormatContext): integer;
public
function GetName: String;
function InitializeDecoder(): boolean;
function FinalizeDecoder(): boolean;
function Open(const Filename: string): TAudioDecodeStream;
end;
function DecodeThreadMain(streamPtr: Pointer): integer; cdecl; forward;
var
singleton_AudioDecoderFFMpeg : IAudioDecoder;
{ TFFMpegDecodeStream }
constructor TFFMpegDecodeStream.Create(pFormatCtx: PAVFormatContext;
pCodecCtx: PAVCodecContext; pCodec: PAVCodec;
ffmpegStreamIndex : Integer; ffmpegStream: PAVStream);
var
sampleFormat: TAudioSampleFormat;
begin
inherited Create();
packetQueue := TPacketQueue.Create();
audio_pkt_data := nil;
audio_pkt_size := 0;
audio_buf_index := 0;
audio_buf_size := 0;
FillChar(pkt, sizeof(TAVPacket), 0);
Self.pFormatCtx := pFormatCtx;
Self.pCodecCtx := pCodecCtx;
Self.pCodec := pCodec;
Self.ffmpegStreamIndex := ffmpegStreamIndex;
Self.ffmpegStream := ffmpegStream;
case pCodecCtx^.sample_fmt of
SAMPLE_FMT_U8: sampleFormat := asfU8;
SAMPLE_FMT_S16: sampleFormat := asfS16;
SAMPLE_FMT_S24: sampleFormat := asfS24;
SAMPLE_FMT_S32: sampleFormat := asfS32;
SAMPLE_FMT_FLT: sampleFormat := asfFloat;
else sampleFormat := asfS16; // try standard format
end;
formatInfo := TAudioFormatInfo.Create(
pCodecCtx^.channels,
pCodecCtx^.sample_rate,
sampleFormat
);
EOFState := false;
ErrorState := false;
decoderLock := SDL_CreateMutex();
parserLock := SDL_CreateMutex();
resumeCond := SDL_CreateCond();
parseThread := SDL_CreateThread(@DecodeThreadMain, Self);
end;
{*
* Frees the decode-stream data.
* IMPORTANT: call Close() before freeing the decode-stream to avoid dead-locks.
* This wakes-up every waiting audio-thread waiting in the packet-queue while
* performing a ReadData() request.
* Then assure that no thread uses ReadData anymore (e.g. by stopping the audio-callback).
* Now you can free the decode-stream.
*}
destructor TFFMpegDecodeStream.Destroy();
begin
// wake-up and terminate threads
// Note: should be called by the caller before Destroy() was called instead
// to wake-up a waiting audio-callback thread in the packet-queue.
// Otherwise dead-locks are possible.
Close();
// Close the codec
if (pCodecCtx <> nil) then
begin
avcodec_close(pCodecCtx);
pCodecCtx := nil;
end;
// Close the video file
if (pFormatCtx <> nil) then
begin
av_close_input_file(pFormatCtx);
pFormatCtx := nil;
end;
FreeAndNil(packetQueue);
FreeAndNil(formatInfo);
SDL_DestroyMutex(decoderLock);
decoderLock := nil;
SDL_DestroyMutex(parserLock);
parserLock := nil;
SDL_DestroyCond(resumeCond);
resumeCond := nil;
inherited;
end;
procedure TFFMpegDecodeStream.Close();
var
status: integer;
begin
// wake threads waiting for packet-queue data
packetQueue.Abort();
// abort parse-thread
LockParser();
quitRequest := true;
SDL_CondSignal(resumeCond);
UnlockParser();
// and wait until it terminates
if (parseThread <> nil) then
begin
SDL_WaitThread(parseThread, status);
parseThread := nil;
end;
// NOTE: we cannot free the codecCtx or formatCtx here because
// a formerly waiting thread in the packet-queue might require them
// and crash if it tries to access them.
end;
procedure TFFMpegDecodeStream.LockParser();
begin
SDL_mutexP(parserLock);
end;
procedure TFFMpegDecodeStream.UnlockParser();
begin
SDL_mutexV(parserLock);
end;
function TFFMpegDecodeStream.GetParserMutex(): PSDL_Mutex;
begin
Result := parserLock;
end;
procedure TFFMpegDecodeStream.LockDecoder();
begin
SDL_mutexP(decoderLock);
end;
procedure TFFMpegDecodeStream.UnlockDecoder();
begin
SDL_mutexV(decoderLock);
end;
function TFFMpegDecodeStream.GetLength(): real;
begin
Result := pFormatCtx^.duration / AV_TIME_BASE;
end;
function TFFMpegDecodeStream.GetAudioFormatInfo(): TAudioFormatInfo;
begin
Result := formatInfo;
end;
function TFFMpegDecodeStream.IsEOF(): boolean;
begin
LockDecoder();
Result := EOFState;
UnlockDecoder();
end;
procedure TFFMpegDecodeStream.SetEOF(state: boolean);
begin
LockDecoder();
EOFState := state;
UnlockDecoder();
end;
function TFFMpegDecodeStream.IsError(): boolean;
begin
LockDecoder();
Result := ErrorState;
UnlockDecoder();
end;
procedure TFFMpegDecodeStream.SetError(state: boolean);
begin
LockDecoder();
ErrorState := state;
UnlockDecoder();
end;
function TFFMpegDecodeStream.GetPosition(): real;
begin
// FIXME: the audio-clock might not be that accurate
// see: tutorial on synching (audio-clock)
Result := audioClock;
end;
procedure TFFMpegDecodeStream.SetPosition(Time: real);
begin
LockParser();
seekPos := Trunc(Time * AV_TIME_BASE);
seekFlags := 0;
// Note: the BACKWARD-flag seeks to the first position <= the position
// searched for. Otherwise e.g. position 0 might not be seeked correct.
// For some reason ffmpeg sometimes doesn't use position 0 but the key-frame
// following. In streams with few key-frames (like many flv-files) the next
// key-frame after 0 might be 5secs ahead.
if (Time < audioClock) then
seekFlags := AVSEEK_FLAG_BACKWARD;
seekFlags := AVSEEK_FLAG_ANY;
seekRequest := true;
SDL_CondSignal(resumeCond);
UnlockParser();
end;
function DecodeThreadMain(streamPtr: Pointer): integer; cdecl;
var
stream: TFFMpegDecodeStream;
begin
stream := TFFMpegDecodeStream(streamPtr);
stream.ParseAudio();
result := 0;
end;
procedure TFFMpegDecodeStream.ParseAudio();
var
packet: TAVPacket;
statusPacket: PAVPacket;
seekTarget: int64;
stopParsing: boolean;
pbIOCtx: PByteIOContext;
err: integer;
index: integer;
begin
stopParsing := false;
while (true) do
begin
LockParser();
// wait if end-of-file reached
if (stopParsing) then
begin
// wait for reuse or destruction of stream
while not (seekRequest or quitRequest) do
SDL_CondWait(resumeCond, GetParserMutex());
end;
if (quitRequest) then
begin
UnlockParser();
break;
end;
// handle seek-request
if (seekRequest) then
begin
// reset status
SetEOF(false);
SetError(false);
stopParsing := false;
seekTarget := av_rescale_q(seekPos, AV_TIME_BASE_Q, ffmpegStream^.time_base);
err := av_seek_frame(pFormatCtx, ffmpegStreamIndex, seekTarget, seekFlags);
// seeking failed -> retry with the default stream (necessary for flv-videos and some ogg-files)
if (err < 0) then
err := av_seek_frame(pFormatCtx, -1, seekPos, seekFlags);
// check if seeking failed
if (err < 0) then
begin
Log.LogStatus('Seek Error in "'+pFormatCtx^.filename+'"', 'UAudioDecoder_FFMpeg');
end
else
begin
packetQueue.Flush();
packetQueue.PutStatus(PKT_STATUS_FLAG_FLUSH, nil);
end;
seekRequest := false;
end;
UnlockParser();
if (packetQueue.size > MAX_AUDIOQ_SIZE) then
begin
SDL_Delay(10);
continue;
end;
if (av_read_frame(pFormatCtx, packet) < 0) then
begin
// failed to read a frame, check reason
{$IF (LIBAVFORMAT_VERSION_MAJOR >= 52)}
pbIOCtx := pFormatCtx^.pb;
{$ELSE}
pbIOCtx := @pFormatCtx^.pb;
{$IFEND}
// check for end-of-file (eof is not an error)
if (url_feof(pbIOCtx) <> 0) then
begin
// signal end-of-file
packetQueue.putStatus(PKT_STATUS_FLAG_EOF, nil);
stopParsing := true;
continue;
end;
// check for errors
if (url_ferror(pbIOCtx) <> 0) then
begin
// an error occured -> abort and wait for repositioning or termination
packetQueue.putStatus(PKT_STATUS_FLAG_ERROR, nil);
stopParsing := true;
continue;
end;
// no error -> wait for user input
SDL_Delay(100);
continue;
end;
if (packet.stream_index = ffmpegStreamIndex) then
begin
packetQueue.put(@packet);
end
else
begin
av_free_packet(@packet);
end;
end;
end;
function TFFMpegDecodeStream.DecodeFrame(var buffer: TAudioBuffer; bufSize: integer): integer;
var
len1,
data_size: integer;
begin
result := -1;
if EOF then
exit;
while(true) do
begin
while (audio_pkt_size > 0) do
begin
data_size := bufSize;
try
{$IF LIBAVCODEC_VERSION >= 51030000} // 51.30.0
len1 := avcodec_decode_audio2(pCodecCtx, @buffer,
data_size, audio_pkt_data, audio_pkt_size);
{$ELSE}
// FIXME: with avcodec_decode_audio a package could contain several frames
// this is not handled yet
len1 := avcodec_decode_audio(pCodecCtx, @buffer,
data_size, audio_pkt_data, audio_pkt_size);
{$IFEND}
except
Log.LogError('Exception at avcodec_decode_audio(2)!', 'TFFMpegDecodeStream.DecodeFrame');
len1 := -1;
end;
if(len1 < 0) then
begin
// if error, skip frame
{$IFDEF DebugFFMpegDecode}
DebugWriteln( 'Skip audio frame' );
{$ENDIF}
audio_pkt_size := 0;
break;
end;
Inc(audio_pkt_data, len1);
Dec(audio_pkt_size, len1);
if (data_size <= 0) then
begin
// no data yet, get more frames
continue;
end;
//pts := audioClock;
audioClock := audioClock + data_size /
(1.0 * formatInfo.FrameSize * formatInfo.SampleRate);
// we have data, return it and come back for more later
result := data_size;
exit;
end;
if (pkt.data <> nil) then
begin
av_free_packet(@pkt);
end;
// do not use an aborted queue
if (packetQueue.abortRequest) then
exit;
// request a new packet and block if non available.
// If this fails, the queue was aborted.
if (packetQueue.Get(pkt, true) < 0) then
exit;
// handle Status-packet
if (PChar(pkt.data) = STATUS_PACKET) then
begin
pkt.data := nil;
audio_pkt_data := nil;
audio_pkt_size := 0;
case (pkt.flags) of
PKT_STATUS_FLAG_FLUSH:
begin
avcodec_flush_buffers(pCodecCtx);
end;
PKT_STATUS_FLAG_EOF: // end-of-file
begin
SetEOF(true);
// buffer contains no data -> result = -1
exit;
end;
PKT_STATUS_FLAG_ERROR:
begin
SetError(true);
Log.LogStatus('I/O Error', 'TFFMpegDecodeStream.DecodeFrame');
exit;
end;
else
begin
Log.LogStatus('Unknown status', 'TFFMpegDecodeStream.DecodeFrame');
end;
end;
continue;
end;
audio_pkt_data := PChar(pkt.data);
audio_pkt_size := pkt.size;
// if available, update the audio clock with pts
if(pkt.pts <> AV_NOPTS_VALUE) then
begin
audioClock := av_q2d(ffmpegStream^.time_base) * pkt.pts;
end;
end;
end;
function TFFMpegDecodeStream.ReadData(Buffer : PChar; BufSize: integer): integer;
var
nBytesCopy: integer; // number of bytes to copy
nBytesRemain: integer; // number of bytes left (remaining) to read
begin
result := -1;
// init number of bytes left to copy to the output buffer
nBytesRemain := BufSize;
// leave if end-of-file was reached previously
if EOF then
exit;
// copy data to output buffer
while (nBytesRemain > 0) do begin
// check if we need more data
if (audio_buf_index >= audio_buf_size) then
begin
// we have already sent all our data; get more
audio_buf_size := DecodeFrame(audio_buf, sizeof(TAudioBuffer));
// check for errors or EOF
if(audio_buf_size < 0) then
begin
// fill decode-buffer with silence
audio_buf_size := 1024;
FillChar(audio_buf, audio_buf_size, #0);
end;
audio_buf_index := 0;
end;
// calc number of new bytes in the decode-buffer
nBytesCopy := audio_buf_size - audio_buf_index;
// resize copy-count if more bytes available than needed (remaining bytes are used the next time)
if (nBytesCopy > nBytesRemain) then
nBytesCopy := nBytesRemain;
Move(audio_buf[audio_buf_index], Buffer[0], nBytesCopy);
Dec(nBytesRemain, nBytesCopy);
Inc(Buffer, nBytesCopy);
Inc(audio_buf_index, nBytesCopy);
end;
Result := BufSize;
end;
{ TAudioDecoder_FFMpeg }
function TAudioDecoder_FFMpeg.GetName: String;
begin
Result := 'FFMpeg_Decoder';
end;
function TAudioDecoder_FFMpeg.InitializeDecoder: boolean;
begin
//Log.LogStatus('InitializeDecoder', 'UAudioDecoder_FFMpeg');
av_register_all();
Result := true;
end;
function TAudioDecoder_FFMpeg.FinalizeDecoder(): boolean;
begin
Result := true;
end;
class function TAudioDecoder_FFMpeg.FindAudioStreamIndex(pFormatCtx : PAVFormatContext): integer;
var
i : integer;
streamIndex: integer;
stream : PAVStream;
begin
// find the first audio stream
streamIndex := -1;
for i := 0 to pFormatCtx^.nb_streams-1 do
begin
//Log.LogStatus('aFormatCtx.streams[i] : ' + inttostr(i), 'UAudio_FFMpeg');
stream := pFormatCtx^.streams[i];
if ( stream.codec^.codec_type = CODEC_TYPE_AUDIO ) then
begin
//Log.LogStatus('Found Audio Stream', 'UAudio_FFMpeg');
streamIndex := i;
break;
end;
end;
Result := streamIndex;
end;
function TAudioDecoder_FFMpeg.Open(const Filename: string): TAudioDecodeStream;
var
pFormatCtx : PAVFormatContext;
pCodecCtx : PAVCodecContext;
pCodec : PAVCodec;
ffmpegStreamID : Integer;
ffmpegStream : PAVStream;
stream : TFFMpegDecodeStream;
begin
Result := nil;
if (not FileExists(Filename)) then
begin
Log.LogStatus('LoadSoundFromFile: Sound not found "' + Filename + '"', 'UAudio_FFMpeg');
exit;
end;
// open audio file
if (av_open_input_file(pFormatCtx, PChar(Filename), nil, 0, nil) <> 0) then
exit;
// TODO: do we need to generate PTS values if they do not exist?
//pFormatCtx^.flags := pFormatCtx^.flags or AVFMT_FLAG_GENPTS;
// retrieve stream information
if (av_find_stream_info(pFormatCtx) < 0) then
exit;
// FIXME: hack used by ffplay. Maybe should not use url_feof() to test for the end
pFormatCtx^.pb.eof_reached := 0;
{$IFDEF DebugFFMpegDecode}
dump_format(pFormatCtx, 0, pchar(Filename), 0);
{$ENDIF}
ffmpegStreamID := FindAudioStreamIndex(pFormatCtx);
if (ffmpegStreamID < 0) then
exit;
//Log.LogStatus('AudioStreamIndex is: '+ inttostr(ffmpegStreamID), 'UAudio_FFMpeg');
ffmpegStream := pFormatCtx.streams[ffmpegStreamID];
pCodecCtx := ffmpegStream^.codec;
pCodec := avcodec_find_decoder(pCodecCtx^.codec_id);
if (pCodec = nil) then
begin
Log.LogStatus('Unsupported codec!', 'UAudio_FFMpeg');
exit;
end;
// set debug options
pCodecCtx^.debug_mv := 0;
pCodecCtx^.debug := 0;
// detect bug-workarounds automatically
pCodecCtx^.workaround_bugs := FF_BUG_AUTODETECT;
// error resilience strategy (careful/compliant/agressive/very_aggressive)
//pCodecCtx^.error_resilience := FF_ER_CAREFUL; //FF_ER_COMPLIANT;
// allow non spec compliant speedup tricks.
//pCodecCtx^.flags2 := pCodecCtx^.flags2 or CODEC_FLAG2_FAST;
// Note: avcodec_open() is not thread-safe!
if (avcodec_open(pCodecCtx, pCodec) < 0) then
begin
Log.LogStatus('avcodec_open failed!', 'UAudio_FFMpeg');
exit;
end;
// TODO: what about pCodecCtx^.start_time? Should we seek to this position here?
// ...
stream := TFFMpegDecodeStream.Create(pFormatCtx, pCodecCtx, pCodec,
ffmpegStreamID, ffmpegStream);
Result := stream;
end;
{ TPacketQueue }
constructor TPacketQueue.Create();
begin
inherited;
firstPkt := nil;
lastPkt := nil;
nbPackets := 0;
size := 0;
mutex := SDL_CreateMutex();
cond := SDL_CreateCond();
end;
destructor TPacketQueue.Destroy();
begin
Flush();
SDL_DestroyMutex(mutex);
SDL_DestroyCond(cond);
inherited;
end;
procedure TPacketQueue.Abort();
begin
SDL_LockMutex(mutex);
abortRequest := true;
SDL_CondSignal(cond);
SDL_UnlockMutex(mutex);
end;
function TPacketQueue.Put(pkt : PAVPacket): integer;
var
pkt1 : PAVPacketList;
begin
result := -1;
if (pkt = nil) then
exit;
if (PChar(pkt^.data) <> STATUS_PACKET) then
begin
if (av_dup_packet(pkt) < 0) then
exit;
end;
pkt1 := av_malloc(sizeof(TAVPacketList));
if (pkt1 = nil) then
exit;
pkt1^.pkt := pkt^;
pkt1^.next := nil;
SDL_LockMutex(Self.mutex);
try
if (Self.lastPkt = nil) then
Self.firstPkt := pkt1
else
Self.lastPkt^.next := pkt1;
Self.lastPkt := pkt1;
inc(Self.nbPackets);
Self.size := Self.size + pkt1^.pkt.size;
SDL_CondSignal(Self.cond);
finally
SDL_UnlockMutex(Self.mutex);
end;
Result := 0;
end;
function TPacketQueue.PutStatus(statusFlag: integer; statusInfo: Pointer): integer;
var
pkt: PAVPacket;
begin
// create temp. package
pkt := av_malloc(SizeOf(TAVPacket));
if (pkt = nil) then
begin
Result := -1;
Exit;
end;
// init package
av_init_packet(pkt^);
pkt^.data := Pointer(STATUS_PACKET);
pkt^.flags := statusFlag;
pkt^.priv := statusInfo;
// put a copy of the package into the queue
Result := Put(pkt);
// data has been copied -> delete temp. package
av_free(pkt);
end;
function TPacketQueue.Get(var pkt: TAVPacket; block: boolean): integer;
var
pkt1 : PAVPacketList;
begin
Result := -1;
SDL_LockMutex(Self.mutex);
try
while true do
begin
if (abortRequest) then
exit;
pkt1 := Self.firstPkt;
if (pkt1 <> nil) then
begin
Self.firstPkt := pkt1^.next;
if (Self.firstPkt = nil) then
Self.lastPkt := nil;
dec(Self.nbPackets);
Self.size := Self.size - pkt1^.pkt.size;
pkt := pkt1^.pkt;
av_free(pkt1);
result := 1;
break;
end
else if (not block) then
begin
result := 0;
break;
end
else
begin
SDL_CondWait(Self.cond, Self.mutex);
end;
end;
finally
SDL_UnlockMutex(Self.mutex);
end;
end;
procedure TPacketQueue.Flush();
var
pkt, pkt1: PAVPacketList;
begin
SDL_LockMutex(Self.mutex);
pkt := Self.firstPkt;
while(pkt <> nil) do
begin
pkt1 := pkt^.next;
av_free_packet(@pkt^.pkt);
// Note: param must be a pointer to a pointer!
av_freep(@pkt);
pkt := pkt1;
end;
Self.lastPkt := nil;
Self.firstPkt := nil;
Self.nbPackets := 0;
Self.size := 0;
SDL_UnlockMutex(Self.mutex);
end;
initialization
singleton_AudioDecoderFFMpeg := TAudioDecoder_FFMpeg.create();
//writeln( 'UAudioDecoder_FFMpeg - Register Decoder' );
AudioManager.add( singleton_AudioDecoderFFMpeg );
finalization
AudioManager.Remove( singleton_AudioDecoderFFMpeg );
end.