From 0b60f71cb853b35e53d0b407493dbcfc098ed40e Mon Sep 17 00:00:00 2001 From: tobigun Date: Tue, 19 Oct 2010 16:39:31 +0000 Subject: initial commit git-svn-id: svn://svn.code.sf.net/p/ultrastardx/svn/branches/experimental@2684 b956fd51-792f-4845-bead-9b4dfca2ff2c --- mediaplugin/src/media/UAudioDecoder_FFmpeg.pas | 1159 ++++++++++++++++++++++++ 1 file changed, 1159 insertions(+) create mode 100644 mediaplugin/src/media/UAudioDecoder_FFmpeg.pas (limited to 'mediaplugin/src/media/UAudioDecoder_FFmpeg.pas') diff --git a/mediaplugin/src/media/UAudioDecoder_FFmpeg.pas b/mediaplugin/src/media/UAudioDecoder_FFmpeg.pas new file mode 100644 index 00000000..b44c7b11 --- /dev/null +++ b/mediaplugin/src/media/UAudioDecoder_FFmpeg.pas @@ -0,0 +1,1159 @@ +{* 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, + mathematics, // used for av_rescale_q + 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. + AUDIO_BUFFER_SIZE = (AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) div 2; + +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; + 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 (av_open_input_file(fFormatCtx, PAnsiChar('ufile:'+FileName.ToUTF8), nil, 0, nil) <> 0) then + begin + Log.LogError('av_open_input_file failed: "' + Filename.ToNative + '"', 'UAudio_FFmpeg'); + Exit; + end; + + // generate PTS values if they do not exist + fFormatCtx^.flags := fFormatCtx^.flags or AVFMT_FLAG_GENPTS; + + // retrieve stream information + if (av_find_stream_info(fFormatCtx) < 0) then + begin + Log.LogError('av_find_stream_info 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'); + + fAudioStream := fFormatCtx.streams[fAudioStreamIndex]; + 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; + + // 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 + AVResult := avcodec_open(fCodecCtx, fCodec); + 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 + av_close_input_file(fFormatCtx); + 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; + ByteIOCtx: PByteIOContext; + ErrorCode: integer; + StartSilence: double; // duration of silence at start of stream + StartSilencePtr: PDouble; // pointer for the EMPTY status packet + + // 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 (url_feof(ByteIOCtx) <> 0) then + 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 (url_ferror(ByteIOCtx) <> 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 ((fFormatCtx^.file_size <> 0) and + (ByteIOCtx^.pos >= fFormatCtx^.file_size)) 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); + avcodec_open(fCodecCtx, fCodec); + 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 >= 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. -- cgit v1.2.3