aboutsummaryrefslogblamecommitdiffstats
path: root/src/media/UAudioDecoder_FFmpeg.pas
blob: bc31954675707ab01a10bb6fb6de3f99b858eb57 (plain) (tree)
























                                                                        
                          

                                                                                








                                                                                 






                
                 
 

                                    
 





                                                                         


              
                                                                                 
          
       
           
          

           
       
           


         
                    
       
          

          
 


                                    
     

                                                                        


                                         
                                                               
        

    
                                                 
           
                               
 

                                                                       
 

                                                     

                                 








                                                                                                           

                                  
                                              
 

                                 
 
                                    
 
                             


                                   
 


                                                                                    

                                  



                                           

                                                           



                                                                          

                                                             


                                   
 
                       













                                                                                 
                                                                             


                                    
          
                           

                                     
                                                    


                                                      
                                                                

                                                      

                                                      
                                                      
                                                      
 
                                                                                    


      
                                                                
          
                               

                                            
                                          
                                                               

      
   
                                

                                                                 

 
                       
 
                                         


                     



                                          
                                       

                                           















                                                                                
                                                       


          
 
                                      
     
                      
 



                        
 


                          
 

                        
 



                                 
 
                                              

    

                                
   
                                         
     
          
 



                                       
                                    

                                        
 
                               



            
                                                                  

                                   
                       






                    
                               
       
                                                                                            
         
      
 
                             
 
                                                  
                                      
                                                                                               

                                                                                              

                                                                                             

                         
       
                                                                                                                               



                                             
                                                              

                                





                                                         
       
                                                                                       

            
      
 







                                                                                       
                                                                                    
                                  
 
                            
                                                              
          
 

                                                                   
       
                                                                                                             



            
                                                                                      
 
                                                    
                                                        
       
                                                                                                 
        

                                   










                                                                        

                                                      
       
                                                        
                     




                      

                           
  
                                     




                                                                                         
          

                                         
                                                  






                                                                            
                           
     
                                        

                                                      
                                                
            
         
                               


                        
                                                          





                                    
                                                                                          



                           
                                   
                                                                                 


                                         


                
                                        

                                    
                                                           

                 

    
                                      
   
                        
     
                                               

                                                                        

                                  

                                            



                                     

                       
                               

                                   

                                               


                    
                            
       
                                         
                             
       
                               
           
                                 
        
                     

      
                         
                             
       
                                        
                                    

                                            

                                               
            
                      



                   

                           

    
                                               
     


                                                                                      
                                                                           

    
                                                                    
     
                        

    
                                              
     


                         
    
 
                                                     
     


                         

    
                                                
     


                         

    
                                                       
     


                         

    
                                                  
     


                         

    
                                               
     


                         

    
                                                 

                        
     



                                                                             

                                                         

                                                                  
                                            

                  

    
                                                      
     
                                      

    
                                                
     


                         

    
                                                        
     


                         

    




                                              
                                            
     
                                                  

         




                                                  
    
 
                                             
     
                                                  

         



                                    

    
                                                                                               
     







                                                                                   
                         
     

                         




                                                                                     
                                    


                                



                                  





                                                                                

                                                       

                                                                                  
                                    
         
                           


                    



                                                 



                                                 
      
    
 
                                                        
   
                              
     
                                      




                         
                                      




                                                         



                                                 
      

    









                                                                          
                                                  
   
                    
                    
                                    
                            


                          


                                                                       

                    








                                                                              




                                                  



                           



                                           

      
     
                 
 
                 
       
                 
       
 
                        
           

                        
          
 
                                                                     
                            

                                              
                                                                        
                          


                                                                                                    



                                                                                                 
                                                       
                            


                                                                                 
            
 



                                                                                     
                               



                                 
                                
                                                                                             


               
                                

                                                                                   
                                   

                                         



                                      



                                                                             
                                          



                                             
                                                                 



                                                                          
                                                        


                                                            
                                                                             


                
                                

                                              
                                 


                          
 
                                                        
           

                      

          
                                                     
           

                                               
                                    
               
                                     


                                                      
                                              
                                          


                                           









                                               
                                                             




                           





                                               

                                                                                
                                                             


               

                                                           
                                               
                                              
               
                                          
                
                                                                  
             
                                                           



                                      
                                                           
             
          
 

                                                       




                                
        

      
 




                                              
                                             
     




                                                   

    
                                              
     



                                     

    
                                                  

                                                                                    
                                         

                                                       
                                     



                                                                       
                                         
                                     
                             
       
                               
                                          

                                            
                                      
              
           
                                 

        

    
                                                                                           
   
                                                                     
                                                                     

                          
                            

                 
     
               
 

               
 
                
       

                                                                 
                                    
         
                                                      
                                       

                                                                              




                         
                                  
         
                             
 

                                                                     
                                            

                                                



                                                                             

                                                                             
             

                                                                            
              
 
                                   
           
                               
                                  
                                         
                
                             
              

          
                                                    
                                             
 


                                                                                 
 
                                                             
                                                                              
      
                                                             

                         

        
                           

                                     
 




                                                                            
 
                                                        
                                            
                                                            
           
 
                           
                                                         
         


                              
 
                                 

                              

                                                                              


                                           


                                     
                                                   
               



                              
                                                                        
               
            

                              


                                                                                                        
           

             
                                                                             


            
               

        

                                        
 
                                                                                        
                                              
         
                                
                                
              
                                                                           
                                
                                                                                       
                                                                            
                                                                                   
              
        


      
                                                                                        
   


                                                                    
 


                              




                                                   



                            



                                            
      
 




                                                     
 

                

                                      
           
 

                                     
         
                                   
                                                   
           
                             

                                                      
                                                                         

                                  
                                     



                              
          
 
                                                      
                                                          



                                                                                                       
 
                                                                            
 
                                         
                                          


                    

      
                       

    
 
                        
 
                                              
     
                             

    
                                                         
     

                                                               
                    















                                                                         

                 
 
                                                         

                 

    
                                                                              
   
                              
     
                
 
                                         
                                     
       
                

         
 
                   
    
 
              
                                                

    
{* UltraStar Deluxe - Karaoke Game
 *
 * UltraStar Deluxe is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * 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 2
 * 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; see the file COPYING. If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 * $URL$
 * $Id$
 *}

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}

// show FFmpeg specific debug output
{.$DEFINE DebugFFmpegDecode}

// FFmpeg is very verbose and shows a bunch of errors.
// Those errors (they can be considered as warnings by us) can be ignored
// as they do not give any useful information.
// There is no solution to fix this except for turning them off.
{.$DEFINE EnableFFmpegErrorOutput}

implementation

uses
  SDL, // SDL redefines some base types -> include before SysUtils to ignore them
  Classes,
  Math,
  SysUtils,
  avcodec,
  avformat,
  avutil,
  avio,
  rational,
  UMusic,
  UIni,
  UMain,
  UMediaCore_FFmpeg,
  ULog,
  UCommon,
  UConfig,
  UPath;

const
  MAX_AUDIOQ_SIZE = (5 * 16 * 1024);

const
  // TODO: The factor 3/2 might not be necessary as we do not need extra
  // space for synchronizing as in the tutorial.
{$IF FFMPEG_VERSION_INT >= 2000000}
  AUDIO_BUFFER_SIZE = (192000 * 3) div 2;
{$ELSE}
  AUDIO_BUFFER_SIZE = (AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) div 2;
{$ENDIF}

type
  TFFmpegDecodeStream = class(TAudioDecodeStream)
    private
      fStateLock:   PSDL_Mutex;

      fEOFState:   boolean; // end-of-stream flag (locked by StateLock)
      fErrorState: boolean; // error flag (locked by StateLock)

      fQuitRequest: boolean; // (locked by StateLock)
      fParserIdleCond: PSDL_Cond;

      // parser pause/resume data
      fParserLocked:            boolean;
      fParserPauseRequestCount: integer;
      fParserUnlockedCond:      PSDL_Cond;
      fParserResumeCond:        PSDL_Cond;

      fSeekRequest: boolean; // (locked by StateLock)
      fSeekFlags:   integer; // (locked by StateLock)
      fSeekPos:     double;    // stream position to seek for (in secs) (locked by StateLock)
      fSeekFlush:   boolean;   // true if the buffers should be flushed after seeking (locked by StateLock)
      SeekFinishedCond: PSDL_Cond;

      fLoop: boolean; // (locked by StateLock)

      fParseThread: PSDL_Thread;
      fPacketQueue: TPacketQueue;

      fFormatInfo: TAudioFormatInfo;

      // FFmpeg specific data
      fFormatCtx: PAVFormatContext;
      fCodecCtx:  PAVCodecContext;
      fCodec:     PAVCodec;

      fAudioStreamIndex: integer;
      fAudioStream: PAVStream;
      fAudioStreamPos: double; // stream position in seconds (locked by DecoderLock)

      // decoder pause/resume data
      fDecoderLocked:            boolean;
      fDecoderPauseRequestCount: integer;
      fDecoderUnlockedCond:      PSDL_Cond;
      fDecoderResumeCond:        PSDL_Cond;

      // state-vars for DecodeFrame (locked by DecoderLock)
      fAudioPaket:        TAVPacket;
      fAudioPaketData:    PByteArray;
      fAudioPaketSize:    integer;
      fAudioPaketSilence: integer; // number of bytes of silence to return

      // state-vars for AudioCallback (locked by DecoderLock)
      fAudioBufferPos:  integer;
      fAudioBufferSize: integer;
      fAudioBuffer:     PByteArray;

      fFilename: IPath;

      procedure SetPositionIntern(Time: real; Flush: boolean; Blocking: boolean);
      procedure SetEOF(State: boolean);   {$IFDEF HasInline}inline;{$ENDIF}
      procedure SetError(State: boolean); {$IFDEF HasInline}inline;{$ENDIF}
      function IsSeeking(): boolean;
      function IsQuit(): boolean;

      procedure Reset();

      procedure Parse();
      function ParseLoop(): boolean;
      procedure PauseParser();
      procedure ResumeParser();

      function DecodeFrame(Buffer: PByteArray; BufferSize: integer): integer;
      procedure FlushCodecBuffers();
      procedure PauseDecoder();
      procedure ResumeDecoder();
    public
      constructor Create();
      destructor Destroy(); override;

      function Open(const Filename: IPath): boolean;
      procedure Close();                     override;

      function GetLength(): real;            override;
      function GetAudioFormatInfo(): TAudioFormatInfo; override;
      function GetPosition: real;            override;
      procedure SetPosition(Time: real);     override;
      function GetLoop(): boolean;           override;
      procedure SetLoop(Enabled: boolean);   override;
      function IsEOF(): boolean;             override;
      function IsError(): boolean;           override;

      function ReadData(Buffer: PByteArray; BufferSize: integer): integer; override;
  end;

type
  TAudioDecoder_FFmpeg = class(TInterfacedObject, IAudioDecoder)
    public
      function GetName: string;

      function InitializeDecoder(): boolean;
      function FinalizeDecoder(): boolean;
      function Open(const Filename: IPath): TAudioDecodeStream;
  end;

var
  FFmpegCore: TMediaCore_FFmpeg;

function ParseThreadMain(Data: Pointer): integer; cdecl; forward;


{ TFFmpegDecodeStream }

constructor TFFmpegDecodeStream.Create();
begin
  inherited Create();

  fStateLock := SDL_CreateMutex();
  fParserUnlockedCond := SDL_CreateCond();
  fParserResumeCond := SDL_CreateCond();
  fParserIdleCond := SDL_CreateCond();
  SeekFinishedCond := SDL_CreateCond();
  fDecoderUnlockedCond := SDL_CreateCond();
  fDecoderResumeCond := SDL_CreateCond();

  // according to the documentation of avcodec_decode_audio(2), sample-data
  // should be aligned on a 16 byte boundary. Otherwise internal calls
  // (e.g. to SSE or Altivec operations) might fail or lack performance on some
  // CPUs. Although GetMem() in Delphi and FPC seems to use a 16 byte or higher
  // alignment for buffers of this size (alignment depends on the size of the
  // requested buffer), we will set the alignment explicitly as the minimum
  // alignment used by Delphi and FPC is on an 8 byte boundary.
  // 
  // Note: AudioBuffer was previously defined as a field of type TAudioBuffer
  // (array[0..AUDIO_BUFFER_SIZE-1] of byte) and hence statically allocated.
  // Fields of records are aligned different to memory allocated with GetMem(),
  // aligning depending on the type but will be at least 2 bytes.
  // AudioBuffer was not aligned to a 16 byte boundary. The {$ALIGN x} directive
  // was not applicable as Delphi in contrast to FPC provides at most 8 byte
  // alignment ({$ALIGN 16} is not supported) by this directive.
  fAudioBuffer := GetAlignedMem(AUDIO_BUFFER_SIZE, 16);

  Reset();
end;

procedure TFFmpegDecodeStream.Reset();
begin
  fParseThread := nil;

  fEOFState := false;
  fErrorState := false;
  fLoop := false;
  fQuitRequest := false;

  fAudioPaketData := nil;
  fAudioPaketSize := 0;
  fAudioPaketSilence := 0;

  fAudioBufferPos := 0;
  fAudioBufferSize := 0;

  fParserLocked := false;
  fParserPauseRequestCount := 0;
  fDecoderLocked := false;
  fDecoderPauseRequestCount := 0;

  FillChar(fAudioPaket, SizeOf(TAVPacket), 0);
end;

{*
 * Frees the decode-stream data.
 *}
destructor TFFmpegDecodeStream.Destroy();
begin
  Close();

  SDL_DestroyMutex(fStateLock);
  SDL_DestroyCond(fParserUnlockedCond);
  SDL_DestroyCond(fParserResumeCond);
  SDL_DestroyCond(fParserIdleCond);
  SDL_DestroyCond(SeekFinishedCond);
  SDL_DestroyCond(fDecoderUnlockedCond);
  SDL_DestroyCond(fDecoderResumeCond);

  FreeAlignedMem(fAudioBuffer);

  inherited;
end;

function TFFmpegDecodeStream.Open(const Filename: IPath): boolean;
var
  SampleFormat: TAudioSampleFormat;
  TestFrame: TAVPacket;
  AVResult: integer;
begin
  Result := false;

  Close();
  Reset();

  if (not Filename.IsFile) then
  begin
    Log.LogError('Audio-file does not exist: "' + Filename.ToNative + '"', 'UAudio_FFmpeg');
    Exit;
  end;

  Self.fFilename := Filename;

  // use custom 'ufile' protocol for UTF-8 support
  {$IF LIBAVFORMAT_VERSION < 53001003}
  AVResult := av_open_input_file(fFormatCtx, PAnsiChar('ufile:'+FileName.ToUTF8), nil, 0, nil);
  {$ELSEIF LIBAVFORMAT_VERSION < 54029104}
  AVResult := avformat_open_input(@fFormatCtx, PAnsiChar('ufile:'+FileName.ToUTF8), nil, nil);
  {$ELSE}
  AVResult := FFmpegCore.AVFormatOpenInput(@fFormatCtx, PAnsiChar('ufile:'+FileName.ToUTF8));
  {$IFEND}
  if (AVResult <> 0) then
  begin
    Log.LogError('Failed to open file "' + Filename.ToNative + '" ('+FFmpegCore.GetErrorString(AVResult)+')', 'UAudio_FFmpeg');
    Exit;
  end;

  // generate PTS values if they do not exist
  fFormatCtx^.flags := fFormatCtx^.flags or AVFMT_FLAG_GENPTS;

  // retrieve stream information
  {$IF LIBAVFORMAT_VERSION >= 53002000)}
  AVResult := avformat_find_stream_info(fFormatCtx, nil);
  {$ELSE}
  AVResult := av_find_stream_info(fFormatCtx);
  {$IFEND}
  if (AVResult < 0) then
  begin
    Log.LogError('No stream info found: "' + Filename.ToNative + '"', 'UAudio_FFmpeg');
    Close();
    Exit;
  end;

{ av_find_stream_info is deprecated and should be replaced by av_read_frame. Untested.
  if (av_read_frame(fFormatCtx, TestFrame) < 0) then
  begin
    Log.LogError('av_read_frame failed: "' + Filename.ToNative + '"', 'UAudio_FFmpeg');
    Close();
    Exit;
  end;
}
  // FIXME: hack used by ffplay. Maybe should not use url_feof() to test for the end
  fFormatCtx^.pb.eof_reached := 0;

  {$IFDEF DebugFFmpegDecode}
  dump_format(fFormatCtx, 0, PAnsiChar(Filename.ToNative), 0);
  {$ENDIF}

  fAudioStreamIndex := FFmpegCore.FindAudioStreamIndex(fFormatCtx);
  if (fAudioStreamIndex < 0) then
  begin
    Log.LogError('FindAudioStreamIndex: No Audio-stream found "' + Filename.ToNative + '"', 'UAudio_FFmpeg');
    Close();
    Exit;
  end;

  //Log.LogStatus('AudioStreamIndex is: '+ inttostr(ffmpegStreamID), 'UAudio_FFmpeg');

{$IF LIBAVFORMAT_VERSION <= 52111000} // <= 52.111.0
  fAudioStream := fFormatCtx.streams[fAudioStreamIndex];
{$ELSE}
  fAudioStream := PPAVStream(PtrUInt(fFormatCtx.streams) + fAudioStreamIndex * Sizeof(pointer))^;
{$IFEND}
  fAudioStreamPos := 0;
  fCodecCtx := fAudioStream^.codec;

  // TODO: should we use this or not? Should we allow 5.1 channel audio?
  (*
  {$IF LIBAVCODEC_VERSION >= 51042000}
  if (CodecCtx^.channels > 0) then
    CodecCtx^.request_channels := Min(2, CodecCtx^.channels)
  else
    CodecCtx^.request_channels := 2;
  {$IFEND}
  *)

  fCodec := avcodec_find_decoder(fCodecCtx^.codec_id);
  if (fCodec = nil) then
  begin
    Log.LogError('Unsupported codec!', 'UAudio_FFmpeg');
    fCodecCtx := nil;
    Close();
    Exit;
  end;

  // set debug options
  fCodecCtx^.debug_mv := 0;
  fCodecCtx^.debug := 0;
  
  {$IF FFMPEG_VERSION_INT >= 1001000}
  // request required sample format
  // reference:
  // http://stackoverflow.com/questions/16479662/ffmpeg-1-0-causing-audio-playback-issues
  // without this avcodec_open2 returns AV_SAMPLE_FMT_S16P
  fCodecCtx^.request_sample_fmt := AV_SAMPLE_FMT_S16;
  {$IFEND}

  // detect bug-workarounds automatically
  fCodecCtx^.workaround_bugs := FF_BUG_AUTODETECT;
  // error resilience strategy (careful/compliant/agressive/very_aggressive)
  //CodecCtx^.error_resilience := FF_ER_CAREFUL; //FF_ER_COMPLIANT;
  // allow non spec compliant speedup tricks.
  //CodecCtx^.flags2 := CodecCtx^.flags2 or CODEC_FLAG2_FAST;

  // Note: avcodec_open() and avcodec_close() are not thread-safe and will
  // fail if called concurrently by different threads.
  FFmpegCore.LockAVCodec();
  try
    {$IF LIBAVCODEC_VERSION >= 53005000}
    AVResult := avcodec_open2(fCodecCtx, fCodec, nil);
    {$ELSE}
    AVResult := avcodec_open(fCodecCtx, fCodec);
    {$IFEND}
  finally
    FFmpegCore.UnlockAVCodec();
  end;
  if (AVResult < 0) then
  begin
    Log.LogError('avcodec_open failed!', 'UAudio_FFmpeg');
    Close();
    Exit;
  end;

  // now initialize the audio-format

  if (not FFmpegCore.ConvertFFmpegToAudioFormat(fCodecCtx^.sample_fmt, SampleFormat)) then
  begin
    // try standard format
    SampleFormat := asfS16;
  end;
  if fCodecCtx^.channels > 255 then
    Log.LogStatus('Error: CodecCtx^.channels > 255', 'TFFmpegDecodeStream.Open');
  fFormatInfo := TAudioFormatInfo.Create(
    byte(fCodecCtx^.channels),
    fCodecCtx^.sample_rate,
    SampleFormat
  );

  fPacketQueue := TPacketQueue.Create();

  // finally start the decode thread
  fParseThread := SDL_CreateThread(@ParseThreadMain, Self);

  Result := true;
end;

procedure TFFmpegDecodeStream.Close();
var
  ThreadResult: integer;
begin
  // wake threads waiting for packet-queue data
  // Note: normally, there are no waiting threads. If there were waiting
  // ones, they would block the audio-callback thread.
  if (assigned(fPacketQueue)) then
    fPacketQueue.Abort();

  // send quit request (to parse-thread etc)
  SDL_mutexP(fStateLock);
  fQuitRequest := true;
  SDL_CondBroadcast(fParserIdleCond);
  SDL_mutexV(fStateLock);

  // abort parse-thread
  if (fParseThread <> nil) then
  begin
    // and wait until it terminates
    SDL_WaitThread(fParseThread, ThreadResult);
    fParseThread := nil;
  end;

  // Close the codec
  if (fCodecCtx <> nil) then
  begin
    // avcodec_close() is not thread-safe
    FFmpegCore.LockAVCodec();
    try
      avcodec_close(fCodecCtx);
    finally
      FFmpegCore.UnlockAVCodec();
    end;
    fCodecCtx := nil;
  end;

  // Close the video file
  if (fFormatCtx <> nil) then
  begin
    {$IF LIBAVFORMAT_VERSION < 53024002}
    av_close_input_file(fFormatCtx);
    {$ELSEIF LIBAVFORMAT_VERSION < 54029104}
    avformat_close_input(@fFormatCtx);
    {$ELSE}
    FFmpegCore.AVFormatCloseInput(@fFormatCtx);
    {$IFEND}
    fFormatCtx := nil;
  end;

  PerformOnClose();
  
  FreeAndNil(fPacketQueue);
  FreeAndNil(fFormatInfo);
end;

function TFFmpegDecodeStream.GetLength(): real;
begin
  // do not forget to consider the start_time value here
  // there is a type size mismatch warnign because start_time and duration are cint64.
  // So, in principle there could be an overflow when doing the sum.
  Result := (fFormatCtx^.start_time + fFormatCtx^.duration) / AV_TIME_BASE;
end;

function TFFmpegDecodeStream.GetAudioFormatInfo(): TAudioFormatInfo;
begin
  Result := fFormatInfo;
end;

function TFFmpegDecodeStream.IsEOF(): boolean;
begin
  SDL_mutexP(fStateLock);
  Result := fEOFState;
  SDL_mutexV(fStateLock);
end;

procedure TFFmpegDecodeStream.SetEOF(State: boolean);
begin
  SDL_mutexP(fStateLock);
  fEOFState := State;
  SDL_mutexV(fStateLock);
end;

function TFFmpegDecodeStream.IsError(): boolean;
begin
  SDL_mutexP(fStateLock);
  Result := fErrorState;
  SDL_mutexV(fStateLock);
end;

procedure TFFmpegDecodeStream.SetError(State: boolean);
begin
  SDL_mutexP(fStateLock);
  fErrorState := State;
  SDL_mutexV(fStateLock);
end;

function TFFmpegDecodeStream.IsSeeking(): boolean;
begin
  SDL_mutexP(fStateLock);
  Result := fSeekRequest;
  SDL_mutexV(fStateLock);
end;

function TFFmpegDecodeStream.IsQuit(): boolean;
begin
  SDL_mutexP(fStateLock);
  Result := fQuitRequest;
  SDL_mutexV(fStateLock);
end;

function TFFmpegDecodeStream.GetPosition(): real;
var
  BufferSizeSec: double;
begin
  PauseDecoder();

  // ReadData() does not return all of the buffer retrieved by DecodeFrame().
  // Determine the size of the unused part of the decode-buffer.
  BufferSizeSec := (fAudioBufferSize - fAudioBufferPos) /
                   fFormatInfo.BytesPerSec;

  // subtract the size of unused buffer-data from the audio clock.
  Result := fAudioStreamPos - BufferSizeSec;

  ResumeDecoder();
end;

procedure TFFmpegDecodeStream.SetPosition(Time: real);
begin
  SetPositionIntern(Time, true, true);
end;

function TFFmpegDecodeStream.GetLoop(): boolean;
begin
  SDL_mutexP(fStateLock);
  Result := fLoop;
  SDL_mutexV(fStateLock);
end;

procedure TFFmpegDecodeStream.SetLoop(Enabled: boolean);
begin
  SDL_mutexP(fStateLock);
  fLoop := Enabled;
  SDL_mutexV(fStateLock);
end;


(********************************************
 * Parser section
 ********************************************)

procedure TFFmpegDecodeStream.PauseParser();
begin
  if (SDL_ThreadID() = fParseThread.threadid) then
    Exit;
    
  SDL_mutexP(fStateLock);
  Inc(fParserPauseRequestCount);
  while (fParserLocked) do
    SDL_CondWait(fParserUnlockedCond, fStateLock);
  SDL_mutexV(fStateLock);
end;

procedure TFFmpegDecodeStream.ResumeParser();
begin
  if (SDL_ThreadID() = fParseThread.threadid) then
    Exit;

  SDL_mutexP(fStateLock);
  Dec(fParserPauseRequestCount);
  SDL_CondSignal(fParserResumeCond);
  SDL_mutexV(fStateLock);
end;

procedure TFFmpegDecodeStream.SetPositionIntern(Time: real; Flush: boolean; Blocking: boolean);
begin
  // - Pause the parser first to prevent it from putting obsolete packages
  //   into the queue after the queue was flushed and before seeking is done.
  //   Otherwise we will hear fragments of old data, if the stream was seeked
  //   in stopped mode and resumed afterwards (applies to non-blocking mode only).
  // - Pause the decoder to avoid race-condition that might occur otherwise.
  // - Last lock the state lock because we are manipulating some shared state-vars.
  PauseParser();
  PauseDecoder();
  SDL_mutexP(fStateLock);
  try
    fEOFState := false;
    fErrorState := false;

    // do not seek if we are already at the correct position.
    // This is important especially for seeking to position 0 if we already are
    // at the beginning. Although seeking with AVSEEK_FLAG_BACKWARD for pos 0 works,
    // it is still a bit choppy (although much better than w/o AVSEEK_FLAG_BACKWARD).
    if (Time = fAudioStreamPos) then
      Exit;    

    // configure seek parameters
    fSeekPos := Time;
    fSeekFlush := Flush;
    fSeekFlags := AVSEEK_FLAG_ANY;
    fSeekRequest := true;

    // 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 <= fAudioStreamPos) then
      fSeekFlags := fSeekFlags or AVSEEK_FLAG_BACKWARD;

    // send a reuse signal in case the parser was stopped (e.g. because of an EOF)
    SDL_CondSignal(fParserIdleCond);
  finally
    SDL_mutexV(fStateLock);
    ResumeDecoder();
    ResumeParser();
  end;

  // in blocking mode, wait until seeking is done
  if (Blocking) then
  begin
    SDL_mutexP(fStateLock);
    while (fSeekRequest) do
      SDL_CondWait(SeekFinishedCond, fStateLock);
    SDL_mutexV(fStateLock);
  end;
end;

function ParseThreadMain(Data: Pointer): integer; cdecl;
var
  Stream: TFFmpegDecodeStream;
begin
  Stream := TFFmpegDecodeStream(Data);
  if (Stream <> nil) then
    Stream.Parse();
  Result := 0;
end;

procedure TFFmpegDecodeStream.Parse();
begin
  // reuse thread as long as the stream is not terminated
  while (ParseLoop()) do
  begin
    // wait for reuse or destruction of stream
    SDL_mutexP(fStateLock);
    while (not (fSeekRequest or fQuitRequest)) do
      SDL_CondWait(fParserIdleCond, fStateLock);
    SDL_mutexV(fStateLock);
  end;
end;

(**
 * Parser main loop.
 * Will not return until parsing of the stream is finished.
 * Reasons for the parser to return are:
 * - the end-of-file is reached
 * - an error occured
 * - the stream was quited (received a quit-request)
 * Returns true if the stream can be resumed or false if the stream has to
 * be terminated.
 *)
function TFFmpegDecodeStream.ParseLoop(): boolean;
var
  Packet: TAVPacket;
  SeekTarget: int64;
  {$IF FFMPEG_VERSION_INT < 1001000}
  ByteIOCtx: PByteIOContext;
  {$ELSE}
  ByteIOCtx: PAVIOContext;
  {$ENDIF}
  ErrorCode: integer;
  StartSilence: double;       // duration of silence at start of stream
  StartSilencePtr: PDouble;  // pointer for the EMPTY status packet 
  fileSize: integer;
  urlError: integer;

  // Note: pthreads wakes threads waiting on a mutex in the order of their
  // priority and not in FIFO order. SDL does not provide any option to
  // control priorities. This might (and already did) starve threads waiting
  // on the mutex (e.g. SetPosition) making usdx look like it was froozen.
  // Instead of simply locking the critical section we set a ParserLocked flag
  // instead and give priority to the threads requesting the parser to pause.
  procedure LockParser();
  begin
    SDL_mutexP(fStateLock);
    while (fParserPauseRequestCount > 0) do
      SDL_CondWait(fParserResumeCond, fStateLock);
    fParserLocked := true;
    SDL_mutexV(fStateLock);
  end;

  procedure UnlockParser();
  begin
    SDL_mutexP(fStateLock);
    fParserLocked := false;
    SDL_CondBroadcast(fParserUnlockedCond);
    SDL_mutexV(fStateLock);
  end;

begin
  Result := true;

  while (true) do
  begin
    LockParser();
    try

      if (IsQuit()) then
      begin
        Result := false;
        Exit;
      end;

      // handle seek-request (Note: no need to lock SeekRequest here)
      if (fSeekRequest) then
      begin
        // first try: seek on the audio stream
        SeekTarget := Round(fSeekPos / av_q2d(fAudioStream^.time_base));
        StartSilence := 0;
        if (SeekTarget < fAudioStream^.start_time) then
          StartSilence := (fAudioStream^.start_time - SeekTarget) * av_q2d(fAudioStream^.time_base);
        ErrorCode := av_seek_frame(fFormatCtx, fAudioStreamIndex, SeekTarget, fSeekFlags);

        if (ErrorCode < 0) then
        begin
          // second try: seek on the default stream (necessary for flv-videos and some ogg-files)
          SeekTarget := Round(fSeekPos * AV_TIME_BASE);
          StartSilence := 0;
          if (SeekTarget < fFormatCtx^.start_time) then
            StartSilence := (fFormatCtx^.start_time - SeekTarget) / AV_TIME_BASE;
          ErrorCode := av_seek_frame(fFormatCtx, -1, SeekTarget, fSeekFlags);
        end;

        // pause decoder and lock state (keep the lock-order to avoid deadlocks).
        // Note that the decoder does not block in the packet-queue in seeking state,
        // so locking the decoder here does not cause a dead-lock.
        PauseDecoder();
        SDL_mutexP(fStateLock);
        try
          if (ErrorCode < 0) then
          begin
            // seeking failed
            fErrorState := true;
            Log.LogError('Seek Error in "'+fFormatCtx^.filename+'"', 'UAudioDecoder_FFmpeg');
          end
          else
          begin
            if (fSeekFlush) then
            begin
              // flush queue (we will send a Flush-Packet when seeking is finished)
              fPacketQueue.Flush();

              // flush the decode buffers
              fAudioBufferSize := 0;
              fAudioBufferPos := 0;
              fAudioPaketSize := 0;
              fAudioPaketSilence := 0;
              FlushCodecBuffers();
              
              // Set preliminary stream position. The position will be set to
              // the correct value as soon as the first packet is decoded.
              fAudioStreamPos := fSeekPos;
            end
            else
            begin
              // request avcodec buffer flush
              fPacketQueue.PutStatus(PKT_STATUS_FLAG_FLUSH, nil);
            end;

            // fill the gap between position 0 and start_time with silence
            // but not if we are in loop mode
            if ((StartSilence > 0) and (not fLoop)) then
            begin
              GetMem(StartSilencePtr, SizeOf(StartSilence));
              StartSilencePtr^ := StartSilence;
              fPacketQueue.PutStatus(PKT_STATUS_FLAG_EMPTY, StartSilencePtr);
            end;
          end;

          fSeekRequest := false;
          SDL_CondBroadcast(SeekFinishedCond);
        finally
          SDL_mutexV(fStateLock);
          ResumeDecoder();
        end;
      end;

      if (fPacketQueue.GetSize() > MAX_AUDIOQ_SIZE) then
      begin
        SDL_Delay(10);
        Continue;
      end;

      if (av_read_frame(fFormatCtx, Packet) < 0) then
      begin
        // failed to read a frame, check reason
        {$IF (LIBAVFORMAT_VERSION_MAJOR >= 52)}
        ByteIOCtx := fFormatCtx^.pb;
        {$ELSE}
        ByteIOCtx := @fFormatCtx^.pb;
        {$IFEND}

        // check for end-of-file (eof is not an error)
        {$IF (LIBAVFORMAT_VERSION_MAJOR < 56)}
        if (url_feof(ByteIOCtx) <> 0) then
        {$ELSE}
        if (avio_feof(ByteIOCtx) <> 0) then
        {$IFEND}
        begin
          if (GetLoop()) then
          begin
            // rewind stream (but do not flush)
            SetPositionIntern(0, false, false);
            Continue;
          end
          else
          begin
            // signal end-of-file
            fPacketQueue.PutStatus(PKT_STATUS_FLAG_EOF, nil);
            Exit;
          end;
        end;

        // check for errors
        {$IF (LIBAVFORMAT_VERSION >= 52103000)}
        urlError := ByteIOCtx^.error;
        {$ELSE}
        urlError := url_ferror(ByteIOCtx);
        {$IFEND}
        if (urlError <> 0) then
        begin
          // an error occured -> abort and wait for repositioning or termination
          fPacketQueue.PutStatus(PKT_STATUS_FLAG_ERROR, nil);
          Exit;
        end;

        // url_feof() does not detect an EOF for some files
        // so we have to do it this way.
        {$IF (LIBAVFORMAT_VERSION >= 53009000)}
        fileSize := avio_size(fFormatCtx^.pb);
        {$ELSE}
        fileSize := fFormatCtx^.file_size;
        {$IFEND}
        if ((fileSize <> 0) and (ByteIOCtx^.pos >= fileSize)) then
        begin
          fPacketQueue.PutStatus(PKT_STATUS_FLAG_EOF, nil);
          Exit;
        end;

        // unknown error occured, exit
        fPacketQueue.PutStatus(PKT_STATUS_FLAG_ERROR, nil);
        Exit;
      end;

      if (Packet.stream_index = fAudioStreamIndex) then
        fPacketQueue.Put(@Packet)
      else
        av_free_packet(@Packet);

    finally
      UnlockParser();
    end;
  end;
end;


(********************************************
 * Decoder section
 ********************************************)

procedure TFFmpegDecodeStream.PauseDecoder();
begin
  SDL_mutexP(fStateLock);
  Inc(fDecoderPauseRequestCount);
  while (fDecoderLocked) do
    SDL_CondWait(fDecoderUnlockedCond, fStateLock);
  SDL_mutexV(fStateLock);
end;

procedure TFFmpegDecodeStream.ResumeDecoder();
begin
  SDL_mutexP(fStateLock);
  Dec(fDecoderPauseRequestCount);
  SDL_CondSignal(fDecoderResumeCond);
  SDL_mutexV(fStateLock);
end;

procedure TFFmpegDecodeStream.FlushCodecBuffers();
begin
  // if no flush operation is specified, avcodec_flush_buffers will not do anything.
  if (@fCodecCtx.codec.flush <> nil) then
  begin
    // flush buffers used by avcodec_decode_audio, etc.
    avcodec_flush_buffers(fCodecCtx);
  end
  else
  begin
    // we need a Workaround to avoid plopping noise with ogg-vorbis and
    // mp3 (in older versions of FFmpeg).
    // We will just reopen the codec.
    FFmpegCore.LockAVCodec();
    try
      avcodec_close(fCodecCtx);
      {$IF LIBAVCODEC_VERSION >= 53005000}
      avcodec_open2(fCodecCtx, fCodec, nil);
      {$ELSE}
      avcodec_open(fCodecCtx, fCodec);
      {$IFEND}
    finally
      FFmpegCore.UnlockAVCodec();
    end;
  end;
end;

function TFFmpegDecodeStream.DecodeFrame(Buffer: PByteArray; BufferSize: integer): integer;
var
  PaketDecodedSize: integer; // size of packet data used for decoding
  DataSize: integer;         // size of output data decoded by FFmpeg
  BlockQueue: boolean;
  SilenceDuration: double;
  {$IFDEF DebugFFmpegDecode}
  TmpPos: double;
  {$ENDIF}
begin
  Result := -1;

  if (EOF) then
    Exit;

  while(true) do
  begin
    // for titles with start_time > 0 we have to generate silence
    // until we reach the pts of the first data packet.
    if (fAudioPaketSilence > 0) then
    begin
      DataSize := Min(fAudioPaketSilence, BufferSize);
      FillChar(Buffer[0], DataSize, 0);
      Dec(fAudioPaketSilence, DataSize);
      fAudioStreamPos := fAudioStreamPos + DataSize / fFormatInfo.BytesPerSec;
      Result := DataSize;
      Exit;
    end;

    // read packet data
    while (fAudioPaketSize > 0) do
    begin
      DataSize := BufferSize;

//      {$IF LIBAVCODEC_VERSION >= 53025000} // 53.25.0
//      PaketDecodedSize := avcodec_decode_audio4(fCodecCtx, AVFrame,
//            @got_frame_ptr, @fAudioPaket);
//      DataSize := AVFrame.nb_samples;
//      Buffer   := PByteArray(AVFrame.data[0]);
      {$IF LIBAVCODEC_VERSION >= 52122000} // 52.122.0
      PaketDecodedSize := avcodec_decode_audio3(fCodecCtx, PSmallint(Buffer),
                  DataSize, @fAudioPaket);
      {$ELSEIF LIBAVCODEC_VERSION >= 51030000} // 51.30.0
      PaketDecodedSize := avcodec_decode_audio2(fCodecCtx, PSmallint(Buffer),
                  DataSize, fAudioPaketData, fAudioPaketSize);
      {$ELSE}
      PaketDecodedSize := avcodec_decode_audio(fCodecCtx, PSmallint(Buffer),
                  DataSize, fAudioPaketData, fAudioPaketSize);
      {$IFEND}

      if(PaketDecodedSize < 0) then
      begin
        // if error, skip frame
        {$IFDEF DebugFFmpegDecode}
        DebugWriteln('Skip audio frame');
        {$ENDIF}
        fAudioPaketSize := 0;
        Break;
      end;

      Inc(PByte(fAudioPaketData), PaketDecodedSize);
      Dec(fAudioPaketSize, PaketDecodedSize);

      // check if avcodec_decode_audio returned data, otherwise fetch more frames
      if (DataSize <= 0) then
        Continue;

      // update stream position by the amount of fetched data
      fAudioStreamPos := fAudioStreamPos + DataSize / fFormatInfo.BytesPerSec;
      
      // we have data, return it and come back for more later
      Result := DataSize;
      Exit;
    end;

    // free old packet data
    if (fAudioPaket.data <> nil) then
      av_free_packet(@fAudioPaket);

    // do not block queue on seeking (to avoid deadlocks on the DecoderLock)
    if (IsSeeking()) then
      BlockQueue := false
    else
      BlockQueue := true;

    // request a new packet and block if none available.
    // If this fails, the queue was aborted.
    if (fPacketQueue.Get(fAudioPaket, BlockQueue) <= 0) then
      Exit;

    // handle Status-packet
    if (PAnsiChar(fAudioPaket.data) = STATUS_PACKET) then
    begin
      fAudioPaket.data := nil;
      fAudioPaketData := nil;
      fAudioPaketSize := 0;

      case (fAudioPaket.flags) of
        PKT_STATUS_FLAG_FLUSH:
        begin
          // just used if SetPositionIntern was called without the flush flag.
          FlushCodecBuffers;
        end;
        PKT_STATUS_FLAG_EOF: // end-of-file
        begin
          // ignore EOF while seeking
          if (not IsSeeking()) then
            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;
        PKT_STATUS_FLAG_EMPTY:
        begin
          SilenceDuration := PDouble(fPacketQueue.GetStatusInfo(fAudioPaket))^;
          fAudioPaketSilence := Round(SilenceDuration * fFormatInfo.SampleRate) * fFormatInfo.FrameSize;
          fPacketQueue.FreeStatusInfo(fAudioPaket);
        end
        else
        begin
          Log.LogStatus('Unknown status', 'TFFmpegDecodeStream.DecodeFrame');
        end;
      end;

      Continue;
    end;

    fAudioPaketData := fAudioPaket.data;
    fAudioPaketSize := fAudioPaket.size;

    // if available, update the stream position to the presentation time of this package
    if(fAudioPaket.pts <> AV_NOPTS_VALUE) then
    begin
      {$IFDEF DebugFFmpegDecode}
      TmpPos := fAudioStreamPos;
      {$ENDIF}
      fAudioStreamPos := av_q2d(fAudioStream^.time_base) * fAudioPaket.pts;
      {$IFDEF DebugFFmpegDecode}
      DebugWriteln('Timestamp: ' + floattostrf(fAudioStreamPos, ffFixed, 15, 3) + ' ' +
                   '(Calc: ' + floattostrf(TmpPos, ffFixed, 15, 3) + '), ' +
                   'Diff: ' + floattostrf(fAudioStreamPos-TmpPos, ffFixed, 15, 3));
      {$ENDIF}
    end;
  end;
end;

function TFFmpegDecodeStream.ReadData(Buffer: PByteArray; BufferSize: integer): integer;
var
  CopyByteCount:   integer; // number of bytes to copy
  RemainByteCount: integer; // number of bytes left (remain) to read
  BufferPos: integer;

  // prioritize pause requests
  procedure LockDecoder();
  begin
    SDL_mutexP(fStateLock);
    while (fDecoderPauseRequestCount > 0) do
      SDL_CondWait(fDecoderResumeCond, fStateLock);
    fDecoderLocked := true;
    SDL_mutexV(fStateLock);
  end;

  procedure UnlockDecoder();
  begin
    SDL_mutexP(fStateLock);
    fDecoderLocked := false;
    SDL_CondBroadcast(fDecoderUnlockedCond);
    SDL_mutexV(fStateLock);
  end;

begin
  Result := -1;

  // set number of bytes to copy to the output buffer
  BufferPos := 0;

  LockDecoder();
  try
    // leave if end-of-file is reached
    if (EOF) then
      Exit;

    // copy data to output buffer
    while (BufferPos < BufferSize) do
    begin
      // check if we need more data
      if (fAudioBufferPos >= fAudioBufferSize) then
      begin
        fAudioBufferPos := 0;

        // we have already sent all our data; get more
        fAudioBufferSize := DecodeFrame(fAudioBuffer, AUDIO_BUFFER_SIZE);

        // check for errors or EOF
        if(fAudioBufferSize < 0) then
        begin
          Result := BufferPos;
          Exit;
        end;
      end;

      // calc number of new bytes in the decode-buffer
      CopyByteCount := fAudioBufferSize - fAudioBufferPos;
      // resize copy-count if more bytes available than needed (remaining bytes are used the next time)
      RemainByteCount := BufferSize - BufferPos;
      if (CopyByteCount > RemainByteCount) then
        CopyByteCount := RemainByteCount;

      Move(fAudioBuffer[fAudioBufferPos], Buffer[BufferPos], CopyByteCount);

      Inc(BufferPos,      CopyByteCount);
      Inc(fAudioBufferPos, CopyByteCount);
    end;
  finally
    UnlockDecoder();
  end;

  Result := BufferSize;
end;


{ TAudioDecoder_FFmpeg }

function TAudioDecoder_FFmpeg.GetName: String;
begin
  Result := 'FFmpeg_Decoder';
end;

function TAudioDecoder_FFmpeg.InitializeDecoder: boolean;
begin
  //Log.LogStatus('InitializeDecoder', 'UAudioDecoder_FFmpeg');
  FFmpegCore := TMediaCore_FFmpeg.GetInstance();
  av_register_all();

  // Do not show uninformative error messages by default.
  // FFmpeg prints all error-infos on the console by default what
  // is very confusing as the playback of the files is correct.
  // We consider these errors to be internal to FFMpeg. They can be fixed
  // by the FFmpeg guys only and do not provide any useful information in
  // respect to USDX.
  {$IFNDEF EnableFFmpegErrorOutput}
    {$IF LIBAVUTIL_VERSION_MAJOR >= 50}
    av_log_set_level(AV_LOG_FATAL);
    {$ELSE}
    // FATAL and ERROR share one log-level, so we have to use QUIET
    av_log_set_level(AV_LOG_QUIET);
    {$IFEND}
  {$ENDIF}

  Result := true;
end;

function TAudioDecoder_FFmpeg.FinalizeDecoder(): boolean;
begin
  Result := true;
end;

function TAudioDecoder_FFmpeg.Open(const Filename: IPath): TAudioDecodeStream;
var
  Stream: TFFmpegDecodeStream;
begin
  Result := nil;

  Stream := TFFmpegDecodeStream.Create();
  if (not Stream.Open(Filename)) then
  begin
    Stream.Free;
    Exit;
  end;

  Result := Stream;
end;

initialization
  MediaManager.Add(TAudioDecoder_FFmpeg.Create);

end.