63

Using this tutorial here: http://www.musicalgeometry.com/?p=1297 I have created a custom overlay and image capture with AVCaptureSession.

I am attempting to allow the user to switch between the front and back camera. Here is my code in CaptureSessionManager to switch cameras:

- (void)addVideoInputFrontCamera:(BOOL)front {
    NSArray *devices = [AVCaptureDevice devices];
    AVCaptureDevice *frontCamera;
    AVCaptureDevice *backCamera;

    for (AVCaptureDevice *device in devices) {

        //NSLog(@"Device name: %@", [device localizedName]);

        if ([device hasMediaType:AVMediaTypeVideo]) {

            if ([device position] == AVCaptureDevicePositionBack) {
                //NSLog(@"Device position : back");
                backCamera = device;
            }
            else {
                //NSLog(@"Device position : front");
                frontCamera = device;
            }
        }
    }

    NSError *error = nil;

    if (front) {
        AVCaptureDeviceInput *frontFacingCameraDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:frontCamera error:&error];
        if (!error) {
            if ([[self captureSession] canAddInput:frontFacingCameraDeviceInput]) {
                [[self captureSession] addInput:frontFacingCameraDeviceInput];
            } else {
                NSLog(@"Couldn't add front facing video input");
            }
        }
    } else {
        AVCaptureDeviceInput *backFacingCameraDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:backCamera error:&error];
        if (!error) {
            if ([[self captureSession] canAddInput:backFacingCameraDeviceInput]) {
                [[self captureSession] addInput:backFacingCameraDeviceInput];
            } else {
                NSLog(@"Couldn't add back facing video input");
            }
        }
    }
}

Now in my custom overlay controller I initialize everything like so in viewDidLoad:

[self setCaptureManager:[[CaptureSessionManager alloc] init]];

[[self captureManager] addVideoInputFrontCamera:NO]; // set to YES for Front Camera, No for Back camera

[[self captureManager] addStillImageOutput];

[[self captureManager] addVideoPreviewLayer];
CGRect layerRect = [[[self view] layer] bounds];
[[[self captureManager] previewLayer] setBounds:layerRect];
[[[self captureManager] previewLayer] setPosition:CGPointMake(CGRectGetMidX(layerRect),CGRectGetMidY(layerRect))];
[[[self view] layer] addSublayer:[[self captureManager] previewLayer]];

[[_captureManager captureSession] startRunning];

The switch camera button is connected to a method called switchCamera. I have tried this:

- (void)switchCameraView:(id)sender {

    [[self captureManager] addVideoInputFrontCamera:YES]; // set to YES for Front Camera, No for Back camera

}

When calling this, I get the error NSLog from the CaptureSessionManager and I cannot figure out why. In viewDidLoad, if I set the fontCamera to YES, it shows the front camera but cannot switch to back, and vice versa.

Any ideas on how to get it to switch properly?

Marwen Doukh
  • 1,946
  • 17
  • 26
Kyle Begeman
  • 7,169
  • 9
  • 40
  • 58
  • My coworker had problems trying to accomplish the same thing. We worked around the issue by initializing 2 separate views for front and back camera. You can call startRunning and stopRunning on the AVCaptureSession and bring the view that you want to use to the front when changing devices. – Mike S Jan 01 '14 at 03:02
  • I have already added an answer Please see [LINK][1] [1]: http://stackoverflow.com/questions/26423953/avcapturesession-addinput-issues-with-ios8/27896067#27896067 – Abdul Yasin Jan 12 '15 at 06:27
  • here is [working solution for swift 3.0.1](http://stackoverflow.com/a/40353301/5147817) – Stepan Maksymov Nov 01 '16 at 02:44

6 Answers6

102

You first need to remove the existing AVCameraInput from the AVCaptureSession and then add a new AVCameraInput to the AVCaptureSession. The following works for me (under ARC):

-(IBAction)switchCameraTapped:(id)sender
{
    //Change camera source
    if(_captureSession)
    {
        //Indicate that some changes will be made to the session
        [_captureSession beginConfiguration];

        //Remove existing input
        AVCaptureInput* currentCameraInput = [_captureSession.inputs objectAtIndex:0];
        [_captureSession removeInput:currentCameraInput];

        //Get new input
        AVCaptureDevice *newCamera = nil;
        if(((AVCaptureDeviceInput*)currentCameraInput).device.position == AVCaptureDevicePositionBack)
        {
            newCamera = [self cameraWithPosition:AVCaptureDevicePositionFront];
        }
        else
        {
            newCamera = [self cameraWithPosition:AVCaptureDevicePositionBack];
        }

        //Add input to session
        NSError *err = nil;
        AVCaptureDeviceInput *newVideoInput = [[AVCaptureDeviceInput alloc] initWithDevice:newCamera error:&err];
        if(!newVideoInput || err)
        {
            NSLog(@"Error creating capture device input: %@", err.localizedDescription);
        }
        else
        {
            [_captureSession addInput:newVideoInput];
        }

        //Commit all the configuration changes at once
        [_captureSession commitConfiguration];
    }
}

// Find a camera with the specified AVCaptureDevicePosition, returning nil if one is not found
- (AVCaptureDevice *) cameraWithPosition:(AVCaptureDevicePosition) position
{
    NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    for (AVCaptureDevice *device in devices) 
    {
        if ([device position] == position) return device;
    }
    return nil;
}
NES_4Life
  • 1,055
  • 1
  • 8
  • 14
  • Is this the answer you were looking for? I've just updated it to include atomic configuration changes! – NES_4Life Mar 20 '14 at 23:08
  • 11
    Just FYI, if you're using AVCaptureSession for video + audio capture, `_captureSession.inputs` will also include an audio input and `[_captureSession.inputs objectAtIndex:0]` may or may not be your camera input. (Index 0 seems to be the first one you add but I don't count on that.) I got around this by having a property for which camera is currently in use and then removing both inputs and re-adding them. – Aaron Brown Aug 14 '14 at 15:46
  • fantastic answer. It is a shame why your answer was never accepted. – Duck Sep 15 '14 at 08:50
  • @NES_4Life as soon as commitConfiguration is called, the delegate method 'didStartRecordingToOutputFileAtURL' is called and that terminates the recording. Do I need to start another recording and merge? – Manish Ahuja Sep 28 '15 at 09:49
  • @ManishAhuja i've never changed camera sources whilst a recording is taking place. I'm not sure this sort of thing is documented by Apple so I'd only be guessing at the correct course of action. I don't think 'didStartRecordingToOutputFileAtURL' terminates the 1st recording but is actually a result of the 2nd recording starting. My guess is that the change of sources (esp. closing the 1st source) is enough to stop the recording. Stitching all recordings together at the end would work if you want a single video. – NES_4Life Sep 28 '15 at 16:22
  • 3
    Brilliant code. But incredibly annoying that us developers have to write code like this, rather than Apple just giving us a flipCamera() function in the SDK. – Mike Gledhill Dec 10 '15 at 08:37
  • Still getting audio lag though it works with camera switching while recording. Has anyone solved this issue..? – Piyush Mathur Mar 07 '19 at 08:05
  • hi, im success change camera but got error in webrtc log: can not encode h264, can you help? – famfamfam Jul 14 '20 at 09:01
39

Swift 4/5

@IBAction func switchCameraTapped(sender: Any) {
    //Change camera source
    if let session = captureSession {
        //Remove existing input
        guard let currentCameraInput: AVCaptureInput = session.inputs.first else {
            return
        }

        //Indicate that some changes will be made to the session
        session.beginConfiguration()
        session.removeInput(currentCameraInput)

        //Get new input
        var newCamera: AVCaptureDevice! = nil
        if let input = currentCameraInput as? AVCaptureDeviceInput {
            if (input.device.position == .back) {
                newCamera = cameraWithPosition(position: .front)
            } else {
                newCamera = cameraWithPosition(position: .back)
            }
        }

        //Add input to session
        var err: NSError?
        var newVideoInput: AVCaptureDeviceInput!
        do {
            newVideoInput = try AVCaptureDeviceInput(device: newCamera)
        } catch let err1 as NSError {
            err = err1
            newVideoInput = nil
        }

        if newVideoInput == nil || err != nil {
            print("Error creating capture device input: \(err?.localizedDescription)")
        } else {
            session.addInput(newVideoInput)
        }

        //Commit all the configuration changes at once
        session.commitConfiguration()
    }
}

// Find a camera with the specified AVCaptureDevicePosition, returning nil if one is not found
func cameraWithPosition(position: AVCaptureDevice.Position) -> AVCaptureDevice? {
    let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: AVMediaType.video, position: .unspecified)
    for device in discoverySession.devices {
        if device.position == position {
            return device
        }
    }

    return nil
}

Swift 3 Edit (Combined with François-Julien Alcaraz answer):

@IBAction func switchCameraTapped(sender: Any) {
    //Change camera source
    if let session = captureSession {
        //Indicate that some changes will be made to the session
        session.beginConfiguration()

        //Remove existing input
        guard let currentCameraInput: AVCaptureInput = session.inputs.first as? AVCaptureInput else {
            return
        }

        session.removeInput(currentCameraInput)

        //Get new input
        var newCamera: AVCaptureDevice! = nil
        if let input = currentCameraInput as? AVCaptureDeviceInput {
            if (input.device.position == .back) {
                newCamera = cameraWithPosition(position: .front)
            } else {
                newCamera = cameraWithPosition(position: .back)
            }
        }

        //Add input to session
        var err: NSError?
        var newVideoInput: AVCaptureDeviceInput!
        do {
            newVideoInput = try AVCaptureDeviceInput(device: newCamera)
        } catch let err1 as NSError {
            err = err1
            newVideoInput = nil
        }

        if newVideoInput == nil || err != nil {
            print("Error creating capture device input: \(err?.localizedDescription)")
        } else {
            session.addInput(newVideoInput)
        }

        //Commit all the configuration changes at once
        session.commitConfiguration()
    }
}

// Find a camera with the specified AVCaptureDevicePosition, returning nil if one is not found
func cameraWithPosition(position: AVCaptureDevicePosition) -> AVCaptureDevice? {
    if let discoverySession = AVCaptureDeviceDiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: AVMediaTypeVideo, position: .unspecified) {
        for device in discoverySession.devices {
            if device.position == position {
                return device
            }
        }
    }

    return nil
}

Swift version to @NES_4Life's answer:

@IBAction func switchCameraTapped(sender: AnyObject) {
    //Change camera source
    if let session = captureSession {
        //Indicate that some changes will be made to the session
        session.beginConfiguration()

        //Remove existing input
        let currentCameraInput:AVCaptureInput = session.inputs.first as! AVCaptureInput
        session.removeInput(currentCameraInput)

        //Get new input
        var newCamera:AVCaptureDevice! = nil
        if let input = currentCameraInput as? AVCaptureDeviceInput {
            if (input.device.position == .Back)
            {
                newCamera = cameraWithPosition(.Front)
            }
            else
            {
                newCamera = cameraWithPosition(.Back)
            }
        }

        //Add input to session
        var err: NSError?
        var newVideoInput: AVCaptureDeviceInput!
        do {
            newVideoInput = try AVCaptureDeviceInput(device: newCamera)
        } catch let err1 as NSError {
            err = err1
            newVideoInput = nil
        }

        if(newVideoInput == nil || err != nil)
        {
            print("Error creating capture device input: \(err!.localizedDescription)")
        }
        else
        {
            session.addInput(newVideoInput)
        }

        //Commit all the configuration changes at once
        session.commitConfiguration()
    }
}

// Find a camera with the specified AVCaptureDevicePosition, returning nil if one is not found
func cameraWithPosition(position: AVCaptureDevicePosition) -> AVCaptureDevice?
{
    let devices = AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo)
    for device in devices {
        let device = device as! AVCaptureDevice
        if device.position == position {
            return device
        }
    }

    return nil
}
chengsam
  • 7,315
  • 6
  • 30
  • 38
  • 2
    -[AVCaptureSession addInput:] Multiple audio/video AVCaptureInputs are not currently supported' – Can Aksoy Oct 08 '16 at 16:53
  • AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo) is deprecated see my comment below – hefgi Feb 10 '17 at 19:18
  • cameraWithPosition method can be modified to look more swfty `return discoverySession.devices.filter { $0.position == position }.first ?? nil` – Volodymyr Smolianinov Feb 18 '19 at 11:30
  • I noticed a `guard` statement that returns without making a call to `session.commitConfiguration()` which may cause some issues. I just wanted to mention this. – ucangetit Oct 15 '19 at 16:43
  • 1
    This worked totally fine just copy and pasting, thank you! Saved me so much time. Gonna modify it a bit to my use-case, but it worked fine in Swift 5. – DavidA Nov 05 '20 at 15:26
  • It is not working for me, Error: Recording Stopped. - some : Error Domain=AVFoundationErrorDomain Code=-11818 "Recording Stopped" UserInfo={AVErrorRecordingSuccessfullyFinishedKey=true, NSLocalizedDescription=Recording Stopped, NSLocalizedRecoverySuggestion=Stop any other actions using the recording device and try again., AVErrorRecordingFailureDomainKey=1, NSUnderlyingError=0x281748090 {Error Domain=NSOSStatusErrorDomain Code=-16414 "(null)"}} – Nitin Mar 03 '21 at 12:27
11

Based on previous answers I made my own version with some validations and one specific change, the current camera input might not be the first object of the capture session's inputs, so I changed this:

//Remove existing input
AVCaptureInput* currentCameraInput = [self.captureSession.inputs objectAtIndex:0];
[self.captureSession removeInput:currentCameraInput];

To this (removing all video type inputs):

for (AVCaptureDeviceInput *input in self.captureSession.inputs) {
    if ([input.device hasMediaType:AVMediaTypeVideo]) {
        [self.captureSession removeInput:input];
        break;
    }
}

Here's the entire code:

if (!self.captureSession) return;

[self.captureSession beginConfiguration];

AVCaptureDeviceInput *currentCameraInput;

// Remove current (video) input
for (AVCaptureDeviceInput *input in self.captureSession.inputs) {
    if ([input.device hasMediaType:AVMediaTypeVideo]) {
        [self.captureSession removeInput:input];

        currentCameraInput = input;
        break;
    }
}

if (!currentCameraInput) return;

// Switch device position
AVCaptureDevicePosition captureDevicePosition = AVCaptureDevicePositionUnspecified;
if (currentCameraInput.device.position == AVCaptureDevicePositionBack) {
    captureDevicePosition = AVCaptureDevicePositionFront;
} else {
    captureDevicePosition = AVCaptureDevicePositionBack;
}

// Select new camera
AVCaptureDevice *newCamera;
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];

for (AVCaptureDevice *captureDevice in devices) {
    if (captureDevice.position == captureDevicePosition) {
        newCamera = captureDevice;
    }
}

if (!newCamera) return;

// Add new camera input
NSError *error;
AVCaptureDeviceInput *newVideoInput = [[AVCaptureDeviceInput alloc] initWithDevice:newCamera error:&error];
if (!error && [self.captureSession canAddInput:newVideoInput]) {
    [self.captureSession addInput:newVideoInput];
}

[self.captureSession commitConfiguration];
Fantini
  • 2,067
  • 21
  • 32
8

Swift 3

func switchCamera() {
        session?.beginConfiguration()
        let currentInput = session?.inputs.first as? AVCaptureDeviceInput
        session?.removeInput(currentInput)

        let newCameraDevice = currentInput?.device.position == .back ? getCamera(with: .front) : getCamera(with: .back)
        let newVideoInput = try? AVCaptureDeviceInput(device: newCameraDevice)
        session?.addInput(newVideoInput)
        session?.commitConfiguration()
    }

// MARK: - Private
extension CameraService {
    func getCamera(with position: AVCaptureDevicePosition) -> AVCaptureDevice? {
        guard let devices = AVCaptureDevice.devices(withMediaType: AVMediaTypeVideo) as? [AVCaptureDevice] else {
            return nil
        }

        return devices.filter {
            $0.position == position
        }.first
    }
}

Swift 4

You can check full implementation in this gist

Bohdan Savych
  • 3,310
  • 4
  • 28
  • 47
2

Here is an updated version of chengsam's code that includes the fix for 'Multiple audio/video AVCaptureInputs are not currently supported'.

func switchCameraTapped() {
    //Change camera source
    //Indicate that some changes will be made to the session
    session.beginConfiguration()

    //Remove existing input
    guard let currentCameraInput: AVCaptureInput = session.inputs.first else {
        return
    }


    //Get new input
    var newCamera: AVCaptureDevice! = nil
    if let input = currentCameraInput as? AVCaptureDeviceInput {
        if (input.device.position == .back) {
            newCamera = cameraWithPosition(position: .front)
        } else {
            newCamera = cameraWithPosition(position: .back)
        }
    }

    //Add input to session
    var err: NSError?
    var newVideoInput: AVCaptureDeviceInput!
    do {
        newVideoInput = try AVCaptureDeviceInput(device: newCamera)
    } catch let err1 as NSError {
        err = err1
        newVideoInput = nil
    }

    if let inputs = session.inputs as? [AVCaptureDeviceInput] {
        for input in inputs {
            session.removeInput(input)
        }
    }


    if newVideoInput == nil || err != nil {
        print("Error creating capture device input: \(err?.localizedDescription)")
    } else {
        session.addInput(newVideoInput)
    }

    //Commit all the configuration changes at once
    session.commitConfiguration()
}

// Find a camera with the specified AVCaptureDevicePosition, returning nil if one is not found
func cameraWithPosition(position: AVCaptureDevice.Position) -> AVCaptureDevice? {
    let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: AVMediaType.video, position: .unspecified)
    for device in discoverySession.devices {
        if device.position == position {
            return device
        }
    }

    return nil
}
William
  • 81
  • 1
  • 9
  • this method work as expected. but can you help me make the new camerapreview fit the parentview? TKs, i can not find some delegate of size to make it fit parent view – famfamfam Apr 17 '20 at 10:58
0

Swift 3 version of cameraWithPosition without deprecated warning :

    // Find a camera with the specified AVCaptureDevicePosition, returning nil if one is not found
func cameraWithPosition(_ position: AVCaptureDevicePosition) -> AVCaptureDevice?
{
    if let deviceDescoverySession = AVCaptureDeviceDiscoverySession.init(deviceTypes: [AVCaptureDeviceType.builtInWideAngleCamera],
                                                          mediaType: AVMediaTypeVideo,
                                                          position: AVCaptureDevicePosition.unspecified) {

        for device in deviceDescoverySession.devices {
            if device.position == position {
                return device
            }
        }
    }

    return nil
}

If you want, you can also get the new devicesTypes from iPhone 7+ (dual camera) by changing the deviceTypes array.

Here's a good read : https://forums.developer.apple.com/thread/63347

hefgi
  • 484
  • 5
  • 14