Can't use different webcams with 2 separate instances of OFX app

Hi all!

I got an app using OpenCV and OFX and it’s working great, except one of our needs is to have multiple windows correspond to a respective webcam. To achieve this, we were planning on running the app multiple times, with each instance configured to use a different webcam for ofVideoGrabber. However, when there is a USB webcam plugged into my Mac, the second instance of my ofx app throws an error:

log
2020-12-28 17:49:55.701 ofx[73218:4915323] OSXVideoGrabber Init Error: Error Domain=AVFoundationErrorDomain Code=-11815 "Cannot Use USB Color Camera" UserInfo={NSLocalizedRecoverySuggestion=Stop any other actions using the USB Color Camera and try again., AVErrorDeviceKey=<AVCaptureDALDevice: 0x7fa5fb62aa60 [USB Color Camera][0x142000000c456366]>, NSLocalizedDescription=Cannot Use USB Color Camera}
2020-12-28 17:49:55.756 ofx[73218:4915323] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** Can't add a nil AVCaptureInput'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007fff34926b57 __exceptionPreprocess + 250
	1   libobjc.A.dylib                     0x00007fff6d7a05bf objc_exception_throw + 48
	2   AVFoundation                        0x00007fff300f27d0 -[AVCaptureDALDevice supportsAVCaptureSessionPreset:] + 0
	3   AVFoundation                        0x00007fff300cc7a5 -[AVCaptureSession addInput:] + 71
	4   ofx                                 0x000000010632107b -[OSXVideoGrabber initCapture:capWidth:capHeight:] + 3207
	5   ofx                                 0x00000001063226d7 _ZN21ofAVFoundationGrabber5setupEii + 105
	6   ofx                                 0x00000001063272ab _ZN14ofVideoGrabber5setupEiib + 243
	7   ofx                                 0x000000010625214c _ZN5ofApp19cameraDeviceChangedERi + 300
	8   ofx                                 0x000000010625185c _ZN5ofApp5setupEv + 254
	9   ofx                                 0x0000000106253d03 _ZNSt3__110__function6__funcIZN7ofEventI16ofMouseEventArgsNS_15recursive_mutexEE13make_functionI10ofxBaseGuiEENS_10shared_ptrIN2of4priv8FunctionIS3_S4_EEEEPT_MSE_FvRS3_EiEUlPKvSG_E_NS_9allocatorISL_EEFbSK_SG_EEclEOSK_SG_ + 37
	10  ofx                                 0x00000001063bfa5d _ZN7ofEventI11ofEventArgsNSt3__115recursive_mutexEE6notifyERS0_ + 149
	11  ofx                                 0x0000000106352c24 _ZN10ofMainLoop3runENSt3__110shared_ptrI15ofAppBaseWindowEEONS1_I9ofBaseAppEE + 1678
	12  ofx                                 0x0000000106352c6e _ZN10ofMainLoop3runEONSt3__110shared_ptrI9ofBaseAppEE + 58
	13  ofx                                 0x000000010635b408 _Z8ofRunAppP9ofBaseApp + 81
	14  ofx                                 0x0000000106254292 main + 180
	15  libdyld.dylib                       0x00007fff6e948cc9 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
/bin/sh: line 1: 73218 Abort trap: 6           ./ofx
make: *** [RunRelease] Error 134

It fails when I call camera.initGrabber(1280, 720);, even if I’ve switched to use a different webcam.

Other info:

  • This error is somewhat a lie–when I don’t have the USB webcam plugged in, two instances of the app can run off of the built-in webcam on my laptop just fine.
  • Even if I use the webcam through some other app, the first instance always works. Zoom happily shares the webcam with OFX.
  • I’m on a Macbook Pro Retina mid-2015, running Catalina and latest stable openframeworks version 11.0.

Any advice or thoughts would be welcome! Thanks so much.

Source code

//--------------------------------------------------------------
void ofApp::setup(){
    settings.loadFile("settings.xml");

    int cd = settings.getValue("settings:cameraDevice", 0);
    this->cameraDeviceChanged(cd); // needs to be lvalue

    // setup face detection
    haar.setup(
        settings.getValue("settings:detectionModel", ""));
    haar.setScaleHaar(2);

    baseMovie.load(
        settings.getValue("settings:baseVideo", ""));
    baseMovie.setLoopState(OF_LOOP_NORMAL);
    baseMovie.play();

    overlayMovie.load(
        settings.getValue("settings:overlayVideo", ""));
    overlayMovie.setLoopState(OF_LOOP_NORMAL);
    overlayMovie.play();

    cameraDevice.addListener(this, &ofApp::cameraDeviceChanged);

    gui.setup(); // most of the time you don't need a name
    gui.add(cameraDevice.set("cameraDevice", settings.getValue("settings:cameraDevice", 1), 0, 10));
    gui.add(increasePresenceSpeed.setup("increasePresenceSpeed", settings.getValue("settings:increasePresenceSpeed", 2.3), 0, 10));
    gui.add(decreasePresenceSpeed.setup("decreasePresenceSpeed", settings.getValue("settings:decreasePresenceSpeed", .2), 0, 10));

    gui.add(minPlaybackSpeed.setup("minPlaybackSpeed", settings.getValue("settings:minPlaybackSpeed", .4), 0, 3));
    gui.add(maxPlaybackSpeed.setup("maxPlaybackSpeed", settings.getValue("settings:maxPlaybackSpeed", 2.3), 0, 3));
    gui.add(minOverlayOpacity.setup("minOverlayOpacity", settings.getValue("settings:minOverlayOpacity", .3), 0, 1));
    gui.add(maxOverlayOpacity.setup("maxOverlayOpacity", settings.getValue("settings:maxOverlayOpacity", 1), 0, 1));

    gui.add(presenceDebounceDelay.setup("presenceDebounceDelay", settings.getValue("settings:presenceDebounceDelay", .2), 0, 10));
    
    gui.loadFromFile("settings.xml");
}

//--------------------------------------------------------------
void ofApp::update(){
    baseMovie.update();
    overlayMovie.update();
    if (camera.isInitialized()) {
      camera.update();

      if(camera.isFrameNew()) {
          // update textures
          rgbImage.setFromPixels(camera.getPixels());
          bwImage = rgbImage;

          haar.findHaarObjects(bwImage);
      }
    }

    double dt = ofGetLastFrameTime();
    if (haar.blobs.size() > 0) {
        presenceDebounceCounter = presenceDebounceDelay;
    }
    else if(presenceDebounceCounter > 0) {
        presenceDebounceCounter -= dt;
    }

    if (presenceDebounceCounter > 0){
        currentPresence += dt * increasePresenceSpeed;
    }
    else {
        currentPresence -= dt * decreasePresenceSpeed;
    }
    currentPresence = ofClamp(currentPresence, 0, 1);

    baseMovie.setSpeed(minPlaybackSpeed + (maxPlaybackSpeed - minPlaybackSpeed) * currentPresence);
    overlayOpacityValue = 255 * (minOverlayOpacity + (maxOverlayOpacity - minOverlayOpacity) * currentPresence);
}

//--------------------------------------------------------------
void ofApp::draw(){
    ofSetColor(ofColor::white);
    baseMovie.draw(0,0, ofGetWindowWidth(),ofGetWindowHeight());
    ofSetColor(255,255,255, overlayOpacityValue);
    overlayMovie.draw(0,0, ofGetWindowWidth(),ofGetWindowHeight());

    if (settings.getValue("settings:debug",0) == 1) {
        ofSetColor(ofColor::white);
        bwImage.draw(0, 0);

        for (int i = 0; i < haar.blobs.size(); i++) {
            ofRectangle rect;
            rect = haar.blobs[i].boundingRect;

            ofSetColor(ofColor::cadetBlue);
            ofNoFill();
            ofDrawRectangle(rect);
        }
        gui.draw();
    }

}

//--------------------------------------------------------------
void ofApp::cameraDeviceChanged(int & cd){
    camera.setDeviceID(cd);
    camera.initGrabber(1280, 720, false);

    // create camera textures
    rgbImage.allocate(camera.getWidth(), camera.getHeight());
    bwImage.allocate(camera.getWidth(), camera.getHeight());
}

//--------------------------------------------------------------
void ofApp::keyPressed(int key){
    if (settings.getValue("settings:debug",0) == 1) {
        settings.setValue("settings:debug",0);
    }
    else {
        settings.setValue("settings:debug",1);
    }
    settings.saveFile("settings.xml");
}

//--------------------------------------------------------------
void ofApp::keyReleased(int key){

}

//--------------------------------------------------------------
void ofApp::mouseMoved(int x, int y ){

}

//--------------------------------------------------------------
void ofApp::mouseDragged(int x, int y, int button){

}

//--------------------------------------------------------------
void ofApp::mousePressed(int x, int y, int button){

}

//--------------------------------------------------------------
void ofApp::mouseReleased(int x, int y, int button){

}

//--------------------------------------------------------------
void ofApp::mouseEntered(int x, int y){

}

//--------------------------------------------------------------
void ofApp::mouseExited(int x, int y){

}

//--------------------------------------------------------------
void ofApp::windowResized(int w, int h){

}

//--------------------------------------------------------------
void ofApp::gotMessage(ofMessage msg){

}

//--------------------------------------------------------------
void ofApp::dragEvent(ofDragInfo dragInfo){ 

}

For posterity–I ended up switching to a multi-window setup. It’s surprisingly simple to work with, for anyone coming here from the future look at the multi window examples that come with OpenFrameworks 11