// ------------------------------------------------------------------------------
// DPF.iOS.Media Camera Component (Capture Video + Audio With Native API)
//
// Dadeh Pardazane Faragir ( DPF ) Co.
//
// Web: http://www.dpfaragir.com
//
// Developed By: Babak Yaghoobi
//
// Email #1: yaghoobi@dpfaragir.com
// Email #2: b_yaghobi@yahoo.com
//
// ------------------------------------------------------------------------------
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// ------------------------------------------------------------------------------

unit DPF.iOS.Media;

interface

{$I DPF.iOS.Defs.inc}

uses
{$IFDEF IOS}
  Macapi.ObjectiveC,
  Macapi.CoreFoundation,
  Macapi.ObjCRuntime,
  Posix.SysSocket,
  Macapi.Dispatch,
  iOSapi.Foundation,
  iOSapi.CocoaTypes,
  Macapi.Mach,
  iOSapi.UIKit,
  iOSapi.AVFoundation,
  iOSapi.CoreMedia,
  iOSapi.CoreVideo,
  iOSapi.CoreGraphics,
  DPF.iOS.AVFoundationConsts,
  iOSapi.CoreAudio,
  DPF.iOS.Classes,
  DPF.iOS.Common,
  DPF.iOS.NSTimer,
{$ENDIF}
  DPF.iOS.UIView,
  System.SysUtils,
  System.Classes,
  System.Types,
  System.UITypes,
  System.TypInfo,
  FMX.Types,
  FMX.PixelFormats,
{$IFDEF DELPHIXE5} FMX.Graphics, {$ENDIF}
  FMX.Dialogs;

// ------------------------------------------------------------------------------

type

  TDPFCamera = class;

  TSampleBufferReadyEvent = procedure( Sender: TObject; const ATime: Int64 ) of object;

  TCameraPosition      = ( cpUnspecified, cpBack, cpFront );
  TVideoCaptureQuality = ( vcqPhotoQuality, vcqHighQuality, vcqMediumQuality, vcqLowQuality, vcq320x240, vcq352x288Quality, vcq640x480Quality, vcq1280x720Quality, vcq1920x1080Quality );

{$IFDEF IOS}

  TDPFFinishWritingThread = class( TThread )
  protected
    FDPFCamera: TDPFCamera;
    procedure Execute; override;
  public
  end;

  TExportCompletionHandler = procedure of object;

  // ------------------------------------------------------------------------------
  ICaptureSessionStoppedRunningNotification = interface( IObjectiveC )
    ['{3361E266-FAE4-4BE8-B8EB-6A45C4C7843D}']
    procedure CaptureSessionStoppedRunningNotification( notification: NSNotification ); cdecl;
  end;

  TCaptureSessionStoppedRunningNotification = class( TOCLocal, ICaptureSessionStoppedRunningNotification )
  private
    FDPFCamera: TDPFCamera;
  public
    constructor create( ADPFCamera: TDPFCamera );

    procedure CaptureSessionStoppedRunningNotification( notification: NSNotification ); cdecl;
  end;

  // ------------------------------------------------------------------------------
  TFinishWritingWithCompletionHandler = procedure of object;

  AVAssetWriterClass = interface( NSObjectClass )
    ['{6D40766B-E7D4-4AB1-84C0-40854E31DBB1}']
    { class } function assetWriterWithURL( outputURL: NSURL; fileType: NSString; error: NSError ): Pointer; cdecl;
  end;

  AVAssetWriter = interface( NSObject )
    ['{3267019A-FB54-4919-B507-759FF0E92BEF}']
    procedure addInput( input: AVAssetWriterInput ); cdecl;
    function availableMediaTypes: NSArray; cdecl;
    function canAddInput( input: AVAssetWriterInput ): Boolean; cdecl;
    function canApplyOutputSettings( outputSettings: NSDictionary; forMediaType: NSString ): Boolean; cdecl;
    procedure cancelWriting; cdecl;
    procedure endSessionAtSourceTime( endTime: CMTime ); cdecl;
    function error: NSError; cdecl;
    // function finishWriting: Boolean; cdecl;
    procedure finishWritingWithCompletionHandler( handler: TFinishWritingWithCompletionHandler ); cdecl;
    function initWithURL( outputURL: NSURL; fileType: NSString; error: NSError ): Pointer; cdecl;
    function inputs: NSArray; cdecl;
    function metadata: NSArray; cdecl;
    function movieFragmentInterval: CMTime; cdecl;
    function movieTimeScale: CMTimeScale; cdecl;
    function outputFileType: NSString; cdecl;
    function outputURL: NSURL; cdecl;
    procedure setMetadata( metadata: NSArray ); cdecl;
    procedure setMovieFragmentInterval( movieFragmentInterval: CMTime ); cdecl;
    procedure setMovieTimeScale( movieTimeScale: CMTimeScale ); cdecl;
    procedure setShouldOptimizeForNetworkUse( shouldOptimizeForNetworkUse: Boolean ); cdecl;
    function shouldOptimizeForNetworkUse: Boolean; cdecl;
    procedure startSessionAtSourceTime( startTime: CMTime ); cdecl;
    function startWriting: Boolean; cdecl;
    function status: AVAssetWriterStatus; cdecl;
  end;

  TAVAssetWriter = class( TOCGenericImport<AVAssetWriterClass, AVAssetWriter> )
  end;

  // ------------------------------------------------------------------------------
  AVAssetExportSessionClass = interface( NSObjectClass )
    ['{27908DE7-98E1-4BD2-8443-79DD5D9682C2}']
    function allExportPresets: NSArray; cdecl;
    function exportPresetsCompatibleWithAsset( asset: AVAsset ): NSArray; cdecl;
    function exportSessionWithAsset( asset: AVAsset; presetName: NSString ): Pointer; cdecl;
  end;

  AVAssetExportSession = interface( NSObject )
    ['{3A571B34-10BE-49CE-87DB-1ED36C08B159}']
    function asset: AVAsset; cdecl;
    function audioMix: AVAudioMix; cdecl;
    procedure cancelExport; cdecl;
    function error: NSError; cdecl;
    function estimatedOutputFileLength: Int64; cdecl;
    function fileLengthLimit: Int64; cdecl;
    function initWithAsset( asset: AVAsset; presetName: NSString ): Pointer; cdecl;
    function maxDuration: CMTime; cdecl;
    function metadata: NSArray; cdecl;
    function outputFileType: NSString; cdecl;
    function outputURL: NSURL; cdecl;
    function presetName: NSString; cdecl;
    function progress: Single; cdecl;
    procedure setAudioMix( audioMix: AVAudioMix ); cdecl;
    procedure setFileLengthLimit( fileLengthLimit: Int64 ); cdecl;
    procedure setMetadata( metadata: NSArray ); cdecl;
    procedure setOutputFileType( outputFileType: NSString ); cdecl;
    procedure setOutputURL( outputURL: NSURL ); cdecl;
    procedure setShouldOptimizeForNetworkUse( shouldOptimizeForNetworkUse: Boolean ); cdecl;
    procedure setTimeRange( timeRange: CMTimeRange ); cdecl;
    procedure setVideoComposition( videoComposition: AVVideoComposition ); cdecl;
    function shouldOptimizeForNetworkUse: Boolean; cdecl;
    function status: AVAssetExportSessionStatus; cdecl;
    function supportedFileTypes: NSArray; cdecl;
    function timeRange: CMTimeRange; cdecl;
    function videoComposition: AVVideoComposition; cdecl;
    procedure exportAsynchronouslyWithCompletionHandler( exportCompletionHandler: TExportCompletionHandler ); cdecl;
  end;

  TAVAssetExportSession = class( TOCGenericImport<AVAssetExportSessionClass, AVAssetExportSession> )
  end;

  // ------------------------------------------------------------------------------
  TVideoAudioSampleDelegate = class( TOCLocal, AVCaptureVideoDataOutputSampleBufferDelegate )
  public
    FDPFCamera: TDPFCamera;
    procedure captureOutput( captureOutput: AVCaptureOutput; didOutputSampleBuffer: CMSampleBufferRef; fromConnection: AVCaptureConnection ); cdecl;
  end;

{$ENDIF}

  // ----------------------------------------------------------------------------
  // TDPFCamera Component
  // ----------------------------------------------------------------------------

  TOnConvertComplete      = procedure( Sender: TObject; Status: Integer ) of object;
  TOnConvertProgressTimer = procedure( Sender: TObject; Progress: Single ) of object;

  [ComponentPlatformsAttribute( PidWin32 or PidWin64 or pidLinux32 or pidAndroid or PidOSX32 or PidiOSDevice or PidiOSSimulator )]
  TDPFCamera = class( TComponent )
  private
    FOnSampleBufferReady: TSampleBufferReadyEvent;
    FPreview            : TDPFUIView;
    FOnConvertComplete  : TOnConvertComplete;
    FVideoCaptureQuality: TVideoCaptureQuality;
    FCameraPosition     : TCameraPosition;
    FOutputFileName     : string;
    FOutputMovFileName  : string;
    FConvertToMP4       : Boolean;
    FOnConvertProgress  : TOnConvertProgressTimer;
{$IFDEF IOS}
    FCurrentDevice: UIDevice;

    FCaptureSessionStoppedRunningNotification: TCaptureSessionStoppedRunningNotification;
    FAVCaptureSession                        : AVCaptureSession;
    FAVCaptureDeviceVideoInput               : AVCaptureDeviceInput;
    FAVCaptureDeviceAudioInput               : AVCaptureDeviceInput;

    FVideoAudioSampleDelegate: TVideoAudioSampleDelegate;

    FImageBuffer : CVImageBufferRef;
    FSampleBuffer: CMSampleBufferRef;

    VideoWriterInput: AVAssetWriterInput;
    AudioWriterInput: AVAssetWriterInput;

    videoOrientation: AVCaptureVideoOrientation;

    AssetWriter: AVAssetWriter;

    currentFrame: Int64;
    maxFrame    : Int64;

    isStopping: Boolean;

    myLock : DPF.iOS.Classes.NSLock;
    myLock2: DPF.iOS.Classes.NSLock;

    FAVCaptureVideoPreviewLayer: AVCaptureVideoPreviewLayer;
    exportSession              : AVAssetExportSession;
    exportSessionTimer         : TDPFNSTimer;
    procedure SyncCallSampleBufferReady;
    procedure exportCompletionHandler;
    function SetupAssetWriters: Boolean;
    procedure DoConvertToMP4;
    procedure OnExportSessionTimerFire( Sender: TObject );
{$ENDIF}
  public
    { Public declarations }
    constructor Create( AOwner: TComponent ); override;
    destructor Destroy; override;

{$IFDEF IOS}
    procedure FinishWritingCompletionHandler;

    function DoSampleBufferToUIImage: UIImage;
    procedure DoSampleBufferToBitmap( ABitmap: TBitmap );
    procedure StartRecording;
    procedure StopRecording;
    procedure PauseRecording;
    procedure ResumeRecording;
    function isRecording: Boolean;
{$ENDIF}
  published
    property Preview            : TDPFUIView read FPreview write FPreview;
    property ConvertToMP4       : Boolean read FConvertToMP4 write FConvertToMP4 default true;
    property OutputFileName     : string read FOutputFileName write FOutputFileName;
    property VideoCaptureQuality: TVideoCaptureQuality read FVideoCaptureQuality write FVideoCaptureQuality default TVideoCaptureQuality.vcqMediumQuality;
    property CameraPosition     : TCameraPosition read FCameraPosition write FCameraPosition default TCameraPosition.cpBack;
    property OnSampleBufferReady: TSampleBufferReadyEvent read FOnSampleBufferReady write FOnSampleBufferReady;
    property OnConvertComplete  : TOnConvertComplete read FOnConvertComplete write FOnConvertComplete;
    property OnConvertProgress  : TOnConvertProgressTimer read FOnConvertProgress write FOnConvertProgress;
  end;

  // ----------------------------------------------------------------------------

{$IFDEF IOS}

procedure UISaveVideoAtPathToSavedPhotosAlbum( videoPath: PNSString; completionTarget: Pointer; completionSelector: SEL; contextInfo: Pointer ); cdecl; external libUIKit name _PU + 'UISaveVideoAtPathToSavedPhotosAlbum';
function UIVideoAtPathIsCompatibleWithSavedPhotosAlbum( videoPath: PNSString ): Boolean; cdecl; external libUIKit name _PU + 'UIVideoAtPathIsCompatibleWithSavedPhotosAlbum';
{$ENDIF}

implementation

{$IFDEF IOS}

// ------------------------------------------------------------------------------
function DegreesToRadians( degrees: CGFloat ): CGFloat;
begin
  result := ( degrees * PI ) / 180;
end;

// ------------------------------------------------------------------------------
function CocoaCMTimeConst( Fwk: string; ConstStr: string ): CMTime;
var
  Obj: Pointer;
begin
  FillChar( result, SizeOf( result ), 0 );
  Obj := Pointer( CocoaPointerConst( Fwk, ConstStr )^ );
  if Obj <> nil then
    Result := CMTime( Obj^ );
end;

{$ENDIF}

// ------------------------------------------------------------------------------
{ TDPFCamera }
constructor TDPFCamera.Create( AOwner: TComponent );
begin
  inherited;
  FVideoCaptureQuality := TVideoCaptureQuality.vcqMediumQuality;
  FCameraPosition      := TCameraPosition.cpBack;
  FConvertToMP4        := true;
{$IFDEF IOS}
  FCurrentDevice                            := TUIDevice.Wrap( TUIDevice.OCClass.currentDevice );
  myLock                                    := TNSLock.Create;
  myLock2                                   := TNSLock.Create;
  isStopping                                := false;
  FCaptureSessionStoppedRunningNotification := TCaptureSessionStoppedRunningNotification.create( self );
  TNSNotificationCenter.Wrap( TNSNotificationCenter.OCClass.defaultCenter ).addObserver( FCaptureSessionStoppedRunningNotification.GetObjectID, sel_getUid( 'CaptureSessionStoppedRunningNotification:' ), ( NSSTR( 'AVCaptureSessionDidStopRunningNotification' ) as ILocalObject ).GetObjectID, nil );
{$ENDIF}
end;

// ------------------------------------------------------------------------------
destructor TDPFCamera.Destroy;
begin
{$IFDEF IOS}
  if Assigned( FAVCaptureSession ) then
  begin
    FAVCaptureSession.release;
    FAVCaptureSession := nil;
  end;
  myLock.release;
  myLock2.release;
  if assigned( FVideoAudioSampleDelegate ) then
    FVideoAudioSampleDelegate.DisposeOf;

  TNSNotificationCenter.Wrap( TNSNotificationCenter.OCClass.defaultCenter ).removeObserver( FCaptureSessionStoppedRunningNotification );
  FCaptureSessionStoppedRunningNotification.DisposeOf;
{$ENDIF}
  inherited;
end;

{$IFDEF IOS}

// ------------------------------------------------------------------------------
procedure TDPFCamera.OnExportSessionTimerFire( Sender: TObject );
begin
  if Assigned( FOnConvertProgress ) then
    FOnConvertProgress( Sender, exportSession.progress );
  if exportSession.progress > 0.99 then
    if Assigned( exportSessionTimer ) then
      exportSessionTimer.Enabled := false;
end;

// ------------------------------------------------------------------------------
procedure TDPFCamera.SyncCallSampleBufferReady;
var
  T: CMTime;
begin
  FImageBuffer := CMSampleBufferGetImageBuffer( FSampleBuffer );
  CVBufferRetain( FImageBuffer );
  try
    T := CMSampleBufferGetPresentationTimeStamp( FSampleBuffer );
    if Assigned( FOnSampleBufferReady ) then
      FOnSampleBufferReady( Self, T.value div T.timescale );
  finally
    CVBufferRelease( FImageBuffer );
    FImageBuffer := 0;
  end;
end;

// ------------------------------------------------------------------------------
function TDPFCamera.isRecording: Boolean;
begin
  result := false;
  if Assigned( FAVCaptureSession ) then
    result := FAVCaptureSession.isRunning;
end;

// ------------------------------------------------------------------------------
procedure TDPFCamera.PauseRecording;
begin
  if Assigned( FAVCaptureSession ) and FAVCaptureSession.isRunning then
    FAVCaptureSession.stopRunning;
end;

// ------------------------------------------------------------------------------
procedure TDPFCamera.ResumeRecording;
begin
  if Assigned( FAVCaptureSession ) and not FAVCaptureSession.isRunning then
    FAVCaptureSession.startRunning;
end;

// ------------------------------------------------------------------------------
procedure TDPFCamera.StopRecording;
begin
  if isStopping then
    exit;

  if not myLock.tryLock then
    exit;
  try
    if isRecording then
    begin
      isStopping := true;
      FAVCaptureSession.stopRunning;
    end;
    currentFrame := 0;
  finally
    myLock.unlock;
  end;
end;

// ------------------------------------------------------------------------------
function TDPFCamera.SetupAssetWriters: Boolean;
var
  error                      : NSError;
  videoCompressionProps      : NSDictionary;
  videoSettings              : NSMutableDictionary;
  rotationDegrees            : CGFloat;
  rotationRadians            : CGFloat;
  outputMovURL               : NSURL;
  acl                        : AudioChannelLayout;
  audioOutputSettings        : NSMutableDictionary;
  preferredHardwareSampleRate: Double;
  CWidth, CHeight            : Integer;
begin
  try
    CWidth  := 640;
    CHeight := 480;
    case FVideoCaptureQuality of
      vcqMediumQuality:
        begin
          CWidth  := 352;
          CHeight := 288;
        end;
      vcqLowQuality:
        begin
          CWidth  := 320;
          CHeight := 240;
        end;
      vcq320x240:
        begin
          CWidth  := 320;
          CHeight := 240;
        end;
      vcq352x288Quality:
        begin
          CWidth  := 352;
          CHeight := 288;
        end;
      vcq640x480Quality:
        begin
          CWidth  := 640;
          CHeight := 480;
        end;
      vcq1280x720Quality:
        begin
          CWidth  := 1280;
          CHeight := 720;
        end;
      vcq1920x1080Quality:
        begin
          CWidth  := 1920;
          CHeight := 1080;
        end;
    end;

    // ---------------------------------------------------------
    // Setup Asset Writer
    // ---------------------------------------------------------
    outputMovURL := TNSURL.Wrap( TNSURL.OCClass.fileURLWithPath( NSStr( FOutputMovFileName ) ) );
    AssetWriter  := TAVAssetWriter.Wrap( TAVAssetWriter.alloc.initWithURL( outputMovURL, AVFileTypeQuickTimeMovie, error ) );
    // Add video input
    videoCompressionProps := TNSDictionary.Wrap( TNSDictionary.OCClass.dictionaryWithObject( TNSNumber.OCClass.numberWithDouble( 512.0 * 1024.0 ), ( AVVideoAverageBitRateKey as ILocalObject ).GetObjectID ) );

    videoSettings := TNSMutableDictionary.Create;
    videoSettings.setObject( ( AVVideoCodecH264 as ILocalObject ).GetObjectID, ( AVVideoCodecKey as ILocalObject ).GetObjectID );
    videoSettings.setObject( TNSNumber.OCClass.numberWithInt( CWidth ), ( AVVideoWidthKey as ILocalObject ).GetObjectID );
    videoSettings.setObject( TNSNumber.OCClass.numberWithInt( CHeight ), ( AVVideoHeightKey as ILocalObject ).GetObjectID );
    // videoSettings.setObject( TNSNumber.OCClass.numberWithInt( 30 ), ( AVVideoMaxKeyFrameIntervalKey as ILocalObject ).GetObjectID );
    videoSettings.setObject( ( videoCompressionProps as ILocalObject ).GetObjectID, ( AVVideoCompressionPropertiesKey as ILocalObject ).GetObjectID );

    VideoWriterInput := TAVAssetWriterInput.Wrap( TAVAssetWriterInput.Alloc.initWithMediaType( AVMediaTypeVideo, videoSettings ) );
    VideoWriterInput.setExpectsMediaDataInRealTime( true );
    videoSettings.release;

    rotationDegrees := 0.0;
    // specify the prefered transform for the output file
    case TUIDevice.Wrap( TUIDevice.OCClass.currentDevice ).orientation of
      UIDeviceOrientationPortraitUpsideDown:
        rotationDegrees := -90.0;
      UIDeviceOrientationLandscapeLeft: // no rotation
        rotationDegrees := 0.0;
      UIDeviceOrientationLandscapeRight:
        rotationDegrees := 180.0;
      UIDeviceOrientationPortrait:
        rotationDegrees := 90.0;
      UIDeviceOrientationUnknown:
        rotationDegrees := 90.0;
      UIDeviceOrientationFaceUp:
        rotationDegrees := 90.0;
      UIDeviceOrientationFaceDown:
        rotationDegrees := 90.0;
    end;
    rotationRadians := DegreesToRadians( rotationDegrees );
    VideoWriterInput.setTransform( CGAffineTransformMakeRotation( rotationRadians ) );

    // Add the audio input
    FillChar( acl, SizeOf( acl ), 0 );

    acl.mChannelLayoutTag := kAudioChannelLayoutTag_Mono;

    preferredHardwareSampleRate := TAVAudioSession.Wrap( TAVAudioSession.OCClass.sharedInstance ).currentHardwareSampleRate;

    audioOutputSettings := TNSMutableDictionary.Create;
    // Both type of audio inputs causes output video file to be corrupted.
    if true then
    begin
      // should work from iphone 3GS on and from ipod 3rd generation
      audioOutputSettings.setObject( TNSNumber.OCClass.numberWithInt( kAudioFormatMPEG4AAC ), ( AVFormatIDKey as ILocalObject ).GetObjectID );
      audioOutputSettings.setObject( TNSNumber.OCClass.numberWithFloat( preferredHardwareSampleRate ), ( AVSampleRateKey as ILocalObject ).GetObjectID );
      audioOutputSettings.setObject( TNSNumber.OCClass.numberWithInt( 64000 ), ( AVEncoderBitRateKey as ILocalObject ).GetObjectID );
      audioOutputSettings.setObject( TNSNumber.OCClass.numberWithInt( 1 ), ( AVNumberOfChannelsKey as ILocalObject ).GetObjectID );
      audioOutputSettings.setObject( TNSData.OCClass.dataWithBytes( @acl, sizeOf( AudioChannelLayout ) ), ( AVChannelLayoutKey as ILocalObject ).GetObjectID );
    end
    else
    begin
      // should work on any device requires more space
      audioOutputSettings.setObject( TNSNumber.OCClass.numberWithInt( kAudioFormatAppleLossless ), ( AVFormatIDKey as ILocalObject ).GetObjectID );
      audioOutputSettings.setObject( TNSNumber.OCClass.numberWithInt( 16 ), ( AVEncoderBitDepthHintKey as ILocalObject ).GetObjectID );
      audioOutputSettings.setObject( TNSNumber.OCClass.numberWithFloat( 44100.0 ), ( AVSampleRateKey as ILocalObject ).GetObjectID );
      audioOutputSettings.setObject( TNSNumber.OCClass.numberWithInt( 1 ), ( AVNumberOfChannelsKey as ILocalObject ).GetObjectID );
      audioOutputSettings.setObject( TNSData.OCClass.dataWithBytes( @acl, sizeOf( acl ) ), ( AVChannelLayoutKey as ILocalObject ).GetObjectID );
    end;

    AudioWriterInput := TAVAssetWriterInput.Wrap( TAVAssetWriterInput.Alloc.initWithMediaType( AVMediaTypeAudio, audioOutputSettings ) );
    AudioWriterInput.setExpectsMediaDataInRealTime( True );
    audioOutputSettings.release;

    // add input
    AssetWriter.addInput( VideoWriterInput );
    AssetWriter.addInput( AudioWriterInput );

    // AssetWriter.startWriting;
    // AssetWriter.startSessionAtSourceTime( nextPTS );
    Result := true;
  except
    Result := false;
  end;

end;

// ------------------------------------------------------------------------------
procedure TDPFCamera.StartRecording;
var
  videoDevices             : NSArray;
  captureVideoDevice       : AVCaptureDevice;
  captureAudioDevice       : AVCaptureDevice;
  I                        : Integer;
  Error                    : NSError;
  VideoQueue               : dispatch_queue_t;
  AudioQueue               : dispatch_queue_t;
  VS                       : NSDictionary;
  outputMovURL             : NSURL;
  videoConnection          : AVCaptureConnection;
  FAVCaptureVideoDataOutput: AVCaptureVideoDataOutput;
  FAVCaptureAudioDataOutput: AVCaptureAudioDataOutput;
begin

  if isStopping then
    exit;

  if not myLock.tryLock then
    exit;
  try
    if assigned( FAVCaptureSession ) then
    begin
      if FAVCaptureSession.isRunning then
        exit;
    end;

    if FConvertToMP4 then
      FOutputMovFileName := GetTempDirectory + 'DPFMovie.mov'
    else
      FOutputMovFileName := FOutputFileName;
    RemoveFile( FOutputMovFileName );
    outputMovURL := TNSURL.Wrap( TNSURL.OCClass.fileURLWithPath( NSStr( FOutputMovFileName ) ) );

    DPF.iOS.Common.RemoveFile( outputMovURL );

    currentFrame := 0;
    maxFrame     := 240;

    if not Assigned( FAVCaptureSession ) then
      FAVCaptureSession := TAVCaptureSession.Create;

    case FVideoCaptureQuality of
      vcqPhotoQuality:
        FAVCaptureSession.setSessionPreset( AVCaptureSessionPresetPhoto );
      vcqHighQuality:
        FAVCaptureSession.setSessionPreset( AVCaptureSessionPresetHigh );
      vcqMediumQuality:
        FAVCaptureSession.setSessionPreset( AVCaptureSessionPresetMedium );
      vcqLowQuality:
        FAVCaptureSession.setSessionPreset( AVCaptureSessionPresetLow );
      vcq320x240:
        FAVCaptureSession.setSessionPreset( AVCaptureSessionPreset320x240 );
      vcq352x288Quality:
        FAVCaptureSession.setSessionPreset( AVCaptureSessionPreset352x288 );
      vcq640x480Quality:
        FAVCaptureSession.setSessionPreset( AVCaptureSessionPreset640x480 );
      vcq1280x720Quality:
        FAVCaptureSession.setSessionPreset( AVCaptureSessionPreset1280x720 );
      vcq1920x1080Quality:
        FAVCaptureSession.setSessionPreset( AVCaptureSessionPreset1920x1080 );
      { vcqiFrame960x540Quality:
        FAVCaptureSession.setSessionPreset( AVCaptureSessionPresetiFrame960x540 );
        vcqiFrame1280x720Quality:
        FAVCaptureSession.setSessionPreset( AVCaptureSessionPresetiFrame1280x720 );
        vcqInputPriorityQuality:
        FAVCaptureSession.setSessionPreset( AVCaptureSessionPresetInputPriority ); }
    end;

    // ----------------------------------------------------
    // Find Video Device
    captureVideoDevice := nil;
    videoDevices       := TAVCaptureDevice.OCClass.devicesWithMediaType( AVMediaTypeVideo );
    for i              := 0 to videoDevices.count - 1 do
    begin
      if TAVCaptureDevice.Wrap( videoDevices.objectAtIndex( i ) ).position = NSInteger( FCameraPosition ) then
      begin
        captureVideoDevice := TAVCaptureDevice.Wrap( videoDevices.objectAtIndex( i ) );
        break;
      end;

      if captureVideoDevice = nil then
        captureVideoDevice := TAVCaptureDevice.Wrap( TAVCaptureDevice.OCClass.defaultDeviceWithMediaType( AVMediaTypeVideo ) );
    end;

    if not Assigned( captureVideoDevice ) then
      exit;

    // ----------------------------------------------------
    // Setup Capture Video Device
    // ----------------------------------------------------
    FAVCaptureDeviceVideoInput := TAVCaptureDeviceInput.Wrap( TAVCaptureDeviceInput.OCClass.deviceInputWithDevice( captureVideoDevice, Error ) );
    FAVCaptureSession.addInput( FAVCaptureDeviceVideoInput );

    FAVCaptureVideoDataOutput := TAVCaptureVideoDataOutput.Create;

    if not Assigned( FVideoAudioSampleDelegate ) then
    begin
      FVideoAudioSampleDelegate            := TVideoAudioSampleDelegate.Create;
      FVideoAudioSampleDelegate.FDPFCamera := Self;
    end;

    VideoQueue := dispatch_queue_create( 'Video Capture Queue', 0 );
    FAVCaptureVideoDataOutput.setSampleBufferDelegate( ILocalObject( FVideoAudioSampleDelegate ).GetObjectID, VideoQueue );
    dispatch_release( VideoQueue );

    VS := TNSDictionary.Wrap( TNSDictionary.OCClass.dictionaryWithObject( TNSNumber.OCClass.numberWithInt( kCVPixelFormatType_32BGRA ), Pointer( kCVPixelBufferPixelFormatTypeKey ) ) );
    FAVCaptureVideoDataOutput.setVideoSettings( VS );
    FAVCaptureVideoDataOutput.setAlwaysDiscardsLateVideoFrames( false );
    FAVCaptureSession.addOutput( FAVCaptureVideoDataOutput );
    videoConnection  := FAVCaptureVideoDataOutput.connectionWithMediaType( AVMediaTypeVideo );
    videoOrientation := videoConnection.videoOrientation;
    FAVCaptureVideoDataOutput.release;

    // ---------------------------------------------------------
    // Setup Preview Layer
    // ---------------------------------------------------------
    if Assigned( FPreview ) then
    begin

      FAVCaptureVideoPreviewLayer := TAVCaptureVideoPreviewLayer.Wrap( TAVCaptureVideoPreviewLayer.Alloc.initWithSession( FAVCaptureSession ) );

      FAVCaptureVideoPreviewLayer.setVideoGravity( AVLayerVideoGravityResizeAspectFill );
      FAVCaptureVideoPreviewLayer.setBounds( FPreview.GetUIView.bounds );
      FAVCaptureVideoPreviewLayer.setPosition( CGPointMake( CGRectGetMidX( FPreview.GetUIView.bounds ), CGRectGetMidY( FPreview.GetUIView.bounds ) ) );

      FPreview.GetUIView.layer.insertSublayer( FAVCaptureVideoPreviewLayer, FPreview.GetUIView.layer );
    end;

    // ----------------------------------------------------
    // Setup Capture Audio Device
    // ----------------------------------------------------
    captureAudioDevice         := TAVCaptureDevice.Wrap( TAVCaptureDevice.OCClass.defaultDeviceWithMediaType( AVMediaTypeAudio ) );
    FAVCaptureDeviceAudioInput := TAVCaptureDeviceInput.Wrap( TAVCaptureDeviceInput.OCClass.deviceInputWithDevice( captureAudioDevice, error ) );
    FAVCaptureSession.addInput( FAVCaptureDeviceAudioInput );

    FAVCaptureAudioDataOutput := TAVCaptureAudioDataOutput.Create;

    AudioQueue := dispatch_queue_create( 'Audio Capture Queue', 0 );
    FAVCaptureAudioDataOutput.setSampleBufferDelegate( ILocalObject( FVideoAudioSampleDelegate ).GetObjectID, AudioQueue );
    dispatch_release( AudioQueue );
    FAVCaptureSession.addOutput( FAVCaptureAudioDataOutput );
    FAVCaptureAudioDataOutput.release;

    SetupAssetWriters;
    // --------------------------------------------------------

    // --------------------------------------------------------
    FAVCaptureSession.startRunning;

    // --------------------------------------------------------
  finally
    myLock.unlock;
  end;
end;

// ------------------------------------------------------------------------------
function TDPFCamera.DoSampleBufferToUIImage: UIImage;
var
  imageBuffer               : CVImageBufferRef;
  baseAddress               : Pointer;
  width, height, bytesPerRow: Longword;
  colorSpace                : CGColorSpaceRef;
  context                   : CGContextRef;
  quartzImage               : CGImageRef;
  ImgOri, OriBack, OriFront : NativeUInt;
begin
  if FSampleBuffer = nil then
    Exit;

  imageBuffer := CMSampleBufferGetImageBuffer( FSampleBuffer );
  CVPixelBufferLockBaseAddress( imageBuffer, 0 );
  baseAddress := CVPixelBufferGetBaseAddress( imageBuffer );
  bytesPerRow := CVPixelBufferGetBytesPerRow( imageBuffer );
  width       := CVPixelBufferGetWidth( imageBuffer );
  height      := CVPixelBufferGetHeight( imageBuffer );
  colorSpace  := CGColorSpaceCreateDeviceRGB( );
  context     := CGBitmapContextCreate( baseAddress, width, height, 8, bytesPerRow, colorSpace, kCGBitmapByteOrder32Little or kCGImageAlphaPremultipliedFirst );

  OriBack  := UIImageOrientationRight;
  OriFront := UIImageOrientationLeftMirrored;

  case FCurrentDevice.orientation of

    UIDeviceOrientationPortrait:
      begin
        OriBack  := UIImageOrientationRight;
        OriFront := UIImageOrientationLeftMirrored;
      end;

    UIDeviceOrientationLandscapeLeft:
      begin
        OriBack  := UIImageOrientationUp;
        OriFront := UIImageOrientationUpMirrored;
      end;

    UIDeviceOrientationLandscapeRight:
      begin
        OriBack  := UIImageOrientationDown;
        OriFront := UIImageOrientationDownMirrored;
      end;
    UIDeviceOrientationPortraitUpsideDown:
      begin
        OriBack  := UIImageOrientationUp;
        OriFront := UIImageOrientationUpMirrored;
      end;
  end;

  quartzImage := CGBitmapContextCreateImage( context );
  CVPixelBufferUnlockBaseAddress( imageBuffer, 0 );
  CGContextRelease( context );
  CGColorSpaceRelease( colorSpace );

  ImgOri := OriBack;
  if FCameraPosition = cpFront then
    ImgOri := OriFront;

  result := TUIImage.Wrap( TUIImage.Alloc.initWithCGImage( quartzImage, 1.0, ImgOri ) );
  // result := TUIImage.Wrap( TUIImage.OCClass.imageWithCGImage( quartzImage ) );
  CGImageRelease( quartzImage );
end;

// ------------------------------------------------------------------------------
procedure TDPFCamera.DoSampleBufferToBitmap( ABitmap: TBitmap );
begin
  ABitmap := UIImageToBitmap( DoSampleBufferToUIImage, 0 );
end;

// ------------------------------------------------------------------------------
procedure TDPFCamera.exportCompletionHandler;
var
  status: Integer;
begin
  status := exportSession.status;

  TThread.Synchronize( Nil,
    procedure
    begin
      if Assigned( FOnConvertComplete ) then
        FOnConvertComplete( Self, status );
    end );
  if Assigned( exportSessionTimer ) then
    exportSessionTimer.DisposeOf;
  exportSession.release;
  exportSession := nil;

end;

// ------------------------------------------------------------------------------
procedure TDPFCamera.DoConvertToMP4;
var
  avAsset          : AVURLAsset;
  compatiblePresets: NSArray;
  outputMP4URL     : NSURL;
  outputMovURL     : NSURL;
  MP4Quality       : NSString;
begin
  outputMP4URL := TNSURL.Wrap( TNSURL.OCClass.fileURLWithPath( NSStr( FOutputFileName ) ) );
  outputMovURL := TNSURL.Wrap( TNSURL.OCClass.fileURLWithPath( NSStr( FOutputMovFileName ) ) );
  DPF.iOS.Common.RemoveFile( outputMP4URL );

  avAsset           := TAVURLAsset.Wrap( TAVURLAsset.OCClass.URLAssetWithURL( outputMovURL, nil ) );
  compatiblePresets := TAVAssetExportSession.OCClass.exportPresetsCompatibleWithAsset( avAsset );

  MP4Quality := AVAssetExportPresetHighestQuality;
  if compatiblePresets.containsObject( ( MP4Quality as ILocalObject ).GetObjectID ) then
  begin
    exportSession := TAVAssetExportSession.Wrap( TAVAssetExportSession.Alloc.initWithAsset( avAsset, MP4Quality ) );
    exportSession.setOutputURL( outputMP4URL );
    exportSession.setShouldOptimizeForNetworkUse( True );
    exportSession.setOutputFileType( AVFileTypeMPEG4 );

    exportSessionTimer := nil;
    if Assigned( FOnConvertProgress ) then
    begin
      exportSessionTimer          := TDPFNSTimer.Create( nil );
      exportSessionTimer.Interval := 100;
      exportSessionTimer.OnTimer  := OnExportSessionTimerFire;
      exportSessionTimer.Enabled  := true;
    end;

    exportSession.exportAsynchronouslyWithCompletionHandler( exportCompletionHandler );
  end;
end;

// ------------------------------------------------------------------------------
procedure TDPFCamera.FinishWritingCompletionHandler;
begin
  { }
end;

// ------------------------------------------------------------------------------
{ TVideoSampleDelegate }
procedure TVideoAudioSampleDelegate.captureOutput( captureOutput: AVCaptureOutput; didOutputSampleBuffer: CMSampleBufferRef; fromConnection: AVCaptureConnection );
var
  formatDescription: CMFormatDescriptionRef;
  mediaType        : CMMediaType;
begin
  try
    if not FDPFCamera.isRecording then
      exit;

    if FDPFCamera.assetWriter = nil then
    begin
      if not FDPFCamera.SetupAssetWriters then
      begin
        DPFNSLog( 'FDPFCamera.SetupAssetWriters: ' + UTF8ToString( FDPFCamera.AssetWriter.error.localizedDescription.UTF8String ) );
        Exit;
      end;
    end;

    formatDescription := CMSampleBufferGetFormatDescription( didOutputSampleBuffer );
    mediaType         := CMFormatDescriptionGetMediaType( formatDescription );

    if FDPFCamera.AssetWriter.status = AVAssetWriterStatusUnknown then
    begin
      if not FDPFCamera.myLock2.tryLock then
        exit;
      try
        if FDPFCamera.AssetWriter.status = AVAssetWriterStatusUnknown then
          if FDPFCamera.AssetWriter.startWriting then
          begin
            FDPFCamera.AssetWriter.startSessionAtSourceTime( CMSampleBufferGetPresentationTimeStamp( didOutputSampleBuffer ) );
          end
          else
          begin
            DPFNSLog( 'AssetWriter.startWriting: ' + UTF8ToString( FDPFCamera.AssetWriter.error.localizedDescription.UTF8String ) );
            exit;
          end
      finally
        FDPFCamera.myLock2.unlock;
      end;
    end;

    if mediaType = kCMMediaType_Video then
    begin
      (*
        case FDPFCamera.FCurrentDevice.orientation of
        AVCaptureVideoOrientationLandscapeLeft:
        fromConnection.setVideoOrientation( AVCaptureVideoOrientationLandscapeLeft );
        AVCaptureVideoOrientationLandscapeRight:
        fromConnection.setVideoOrientation( AVCaptureVideoOrientationLandscapeRight );
        AVCaptureVideoOrientationPortraitUpsideDown:
        fromConnection.setVideoOrientation( AVCaptureVideoOrientationPortraitUpsideDown );
        AVCaptureVideoOrientationPortrait:
        fromConnection.setVideoOrientation( AVCaptureVideoOrientationPortrait );
        end;
      *)

      // Capture Frame
      // FDPFCamera.FSampleBuffer := didOutputSampleBuffer;
      // TThread.Synchronize( Nil, FDPFCamera.SyncCallSampleBufferReady );

      if FDPFCamera.AssetWriter.status = AVAssetWriterStatusWriting then
        if Assigned( FDPFCamera.VideoWriterInput ) and FDPFCamera.VideoWriterInput.isReadyForMoreMediaData then
          if not FDPFCamera.VideoWriterInput.appendSampleBuffer( didOutputSampleBuffer { sbufWithNewTiming } ) then
            DPFNSLog( 'VideoWriterInput.appendSampleBuffer: ' + UTF8ToString( FDPFCamera.AssetWriter.error.localizedDescription.UTF8String ) );

      FDPFCamera.currentFrame := FDPFCamera.currentFrame + 1;

      if FDPFCamera.currentFrame >= FDPFCamera.maxFrame then
      begin

      end;
    end
    else if mediaType = kCMMediaType_Audio then
    begin
      if FDPFCamera.AssetWriter.status = AVAssetWriterStatusWriting then
        if Assigned( FDPFCamera.AudioWriterInput ) and FDPFCamera.AudioWriterInput.isReadyForMoreMediaData then
          if not FDPFCamera.AudioWriterInput.appendSampleBuffer( didOutputSampleBuffer ) then
            DPFNSLog( 'AudioWriterInput.appendSampleBuffer: ' + UTF8ToString( FDPFCamera.AssetWriter.error.localizedDescription.UTF8String ) );
    end;

  except
    on E: Exception do
      DPFNSLog( 'TVideoAudioSampleDelegate.captureOutput: ' + E.Message );
  end;
end;

// ------------------------------------------------------------------------------
constructor TCaptureSessionStoppedRunningNotification.create( ADPFCamera: TDPFCamera );
begin
  inherited create;
  FDPFCamera := ADPFCamera;
end;

// ------------------------------------------------------------------------------
procedure TCaptureSessionStoppedRunningNotification.CaptureSessionStoppedRunningNotification( notification: NSNotification );
var
  Coutput : AVCaptureOutput;
  Cinput  : AVCaptureDeviceInput;
  MyThread: TDPFFinishWritingThread;
begin

  if not Assigned( FDPFCamera.FAVCaptureSession ) or not Assigned( FDPFCamera.AssetWriter ) or not Assigned( FDPFCamera.VideoWriterInput ) or not Assigned( FDPFCamera.AudioWriterInput ) then
    exit;

  MyThread                 := TDPFFinishWritingThread.Create( true );
  MyThread.FDPFCamera      := FDPFCamera;
  MyThread.FreeOnTerminate := true;
  MyThread.Start;

  if Assigned( FDPFCamera.FAVCaptureVideoPreviewLayer ) then
  begin
    FDPFCamera.FAVCaptureVideoPreviewLayer.removeFromSuperlayer;
    FDPFCamera.FAVCaptureVideoPreviewLayer.release;
    FDPFCamera.FAVCaptureVideoPreviewLayer := nil;
  end;

  if Assigned( FDPFCamera.FAVCaptureVideoPreviewLayer ) then
  begin
    FDPFCamera.FAVCaptureVideoPreviewLayer.removeFromSuperlayer;
    FDPFCamera.FAVCaptureVideoPreviewLayer.release;
    FDPFCamera.FAVCaptureVideoPreviewLayer := nil;
  end;
end;

// ------------------------------------------------------------------------------
{ TDPFFinishWritingThread }
procedure TDPFFinishWritingThread.Execute;
var
  Coutput: AVCaptureOutput;
  Cinput : AVCaptureDeviceInput;
begin
  try
    FDPFCamera.VideoWriterInput.markAsFinished;
    FDPFCamera.AudioWriterInput.markAsFinished;

    FDPFCamera.AssetWriter.finishWritingWithCompletionHandler( FDPFCamera.FinishWritingCompletionHandler );

    if Assigned( FDPFCamera.FAVCaptureSession.inputs ) then
      while FDPFCamera.FAVCaptureSession.inputs.count > 0 do
      begin
        Cinput := TAVCaptureDeviceInput.Wrap( FDPFCamera.FAVCaptureSession.inputs.objectAtIndex( 0 ) );
        FDPFCamera.FAVCaptureSession.removeInput( Cinput );
      end;

    if Assigned( FDPFCamera.FAVCaptureSession.outputs ) then
      while FDPFCamera.FAVCaptureSession.outputs.count > 0 do
      begin
        Coutput := TAVCaptureOutput.Wrap( FDPFCamera.FAVCaptureSession.outputs.objectAtIndex( 0 ) );
        FDPFCamera.FAVCaptureSession.removeOutput( Coutput );
      end;

    FDPFCamera.VideoWriterInput.release;
    FDPFCamera.VideoWriterInput := nil;

    FDPFCamera.AudioWriterInput.release;
    FDPFCamera.AudioWriterInput := nil;

    FDPFCamera.AssetWriter.release;
    FDPFCamera.AssetWriter := nil;

    FDPFCamera.FAVCaptureSession.release;
    FDPFCamera.FAVCaptureSession := nil;

    if FDPFCamera.FConvertToMP4 then
    begin
      TThread.Synchronize( nil,
        procedure
        begin
          FDPFCamera.DoConvertToMP4;
        end );
    end;

  finally
    FDPFCamera.isStopping := false;
  end;
end;

// ------------------------------------------------------------------------------
{$ENDIF}

// ------------------------------------------------------------------------------
end.
