Implementing an Android Virtual Camera That Multiplexes a Physical Sensor Across Multiple Apps
Android’s camera stack enforces single-owner semantics, which prevents multiple processes from using the same sensor concurrently. A virtual camera layer can fan out frames from one physical device to many clients by routing frames through shared memory and exposing an additional HAL camera device that reads those frames. The end result allows several apps to "open" the same logical camera without racing or evicting each other, and without invoking the physical device more than once.
1. Relaxing CameraService process limits
CameraService tracks active clients and evicts lower-priority ones when resources exceed a configured cost. The cap is enforced by ClientManager in frameworks/av/services/camera.
To raise the ceiling for simultaneous clients, increase the max cost tracked by ClientManager. One approach is to scale the input cost:
// services/camera/libcameraservice/utils/ClientManager.h
// Expand allowable cost to support more active clients from different processes
template <typename K, typename V, typename Listener>
ClientManager<K, V, Listener>::ClientManager(int32_t totalCost)
: mMaxCost(totalCost * 10) {}
Client eviction happens via handleEvictionsLocked in CameraService, which calls mActiveClientManager.wouldEvict(...). A brute-force bypass sets conflicting to false in wouldEvictLocked, preventing same-ID conflict detection at the framework layer. This only avoids framework-level evictions; the HAL will still reject opening the same physical device twice.
// Simplified excerpt: force no-conflict to skip evictions
for (const auto& it : mClients) {
const K& existingKey = it->getKey();
bool conflicting = false; // Force-disable conflict logic
// ... all eviction branches will be skipped when conflicting == false
}
This alone is insufficient, because the HAL will still report "device already opened." The remaining work is to avoid opening the physical camera more than once and instead multiplex via a virtual HAL device.
2. Virtual HAL device that reads from shared memory
Create a new camera HAL module (HALv3 recommended) that looks like a normal camera to the framework but sources frames from shared memory, not hardware. The real physical camera writes frames into this shared region.
Example module skeleton:
// camera_virtual_module.cpp
static int openVirtualDevice(const hw_module_t* mod, const char* name, hw_device_t** dev);
camera_module_t HAL_MODULE_INFO_SYM = {
.common = {
.tag = HARDWARE_MODULE_TAG,
.module_api_version = CAMERA_MODULE_API_VERSION_2_3,
.hal_api_version = HARDWARE_HAL_API_VERSION,
.id = "virtual_camera",
.name = "virtual_camera",
.author = "Virtual Camera",
.methods = new hw_module_methods_t{ .open = openVirtualDevice },
.dso = nullptr,
.reserved = {0},
},
.get_number_of_cameras = android::HalModule::getNumberOfCameras,
.get_camera_info = android::HalModule::getCameraInfo,
.set_callbacks = android::HalModule::setCallbacks,
};
static int openVirtualDevice(const hw_module_t* module, const char* name, hw_device_t** deviceOut) {
ALOGI("%s: openVirtualDevice name=%s", __FUNCTION__, name);
if (module != &HAL_MODULE_INFO_SYM.common || name == nullptr) return -EINVAL;
errno = 0;
int id = static_cast<int>(strtol(name, nullptr, 10));
if (errno != 0 || id < 0 || id >= android::HalModule::getNumberOfCameras()) return -EINVAL;
if (!android::cams[id]->isValid()) {
*deviceOut = nullptr;
return -ENODEV;
}
return android::cams[id]->openDevice(deviceOut);
}
Camera object with camera3_device ops:
class Camera : public camera3_device {
public:
Camera();
virtual ~Camera();
bool isValid() const { return mValid; }
// Module/Device entry points
status_t cameraInfo(camera_info* info);
int openDevice(hw_device_t** dev);
int closeDevice();
protected:
// camera3_device_ops
int initialize(const camera3_callback_ops_t* cb);
int configureStreams(camera3_stream_configuration_t* cfg);
const camera_metadata_t* constructDefaultRequestSettings(int type);
int registerStreamBuffers(const camera3_stream_buffer_set_t* set);
int processCaptureRequest(camera3_capture_request_t* request);
camera_metadata_t* staticCharacteristics();
// Helpers
void notifyShutter(uint32_t frameNumber, uint64_t ts);
void dispatchResult(uint32_t frameNumber, const camera_metadata_t* result,
const Vector<camera3_stream_buffer>& bufs);
private:
// Shared memory reader + conversion workspace
Mutex mLock;
uint8_t* mSharedReadPtr = nullptr; // mmap result
uint8_t* mScratch = nullptr;
size_t mJpegBufferSize = 0;
bool mValid = false;
const camera3_callback_ops_t* mCallbacks = nullptr;
// Static wrappers
static int sInitialize(const camera3_device*, const camera3_callback_ops_t*);
static int sConfigureStreams(const camera3_device*, camera3_stream_configuration_t*);
static const camera_metadata_t* sConstructDefaultRequestSettings(const camera3_device*, int);
static int sRegisterStreamBuffers(const camera3_device*, const camera3_stream_buffer_set_t*);
static int sProcessCaptureRequest(const camera3_device*, camera3_capture_request_t*);
static void sDump(const camera3_device*, int);
static int sFlush(const camera3_device*);
static camera3_device_ops_t sOps;
};
Key differences from a real device:
- openDevice creates/mmap’s a shared region rather than opening the physical sensor.
- processCaptureRequest reads latest frame from shared memory and writes into the output buffers (after format conversion), then calls process_capture_result.
Who writes to shared memory? The physical camera HAL path that owns the hardware does.
Feeding shared memory from the physical HAL
On MTK 6762 (Android 8.1), preview frames traverse DisplayClient. In DisplayClient.BufOps.cpp you can access the current buffer’s virtual address and copy it into shared memory, for example:
// Pseudocode in the real camera HAL path
uint8_t* src = reinterpret_cast<uint8_t*>(pStreamImgBuf->getVirAddr());
memcpy(mappedSharedRegion, src, frameSizeBytes);
For other SoCs (Qualcomm, etc.), hook at the point where HAL surfaces return completed frames and perform the same copy.
Format conversion for preview
Typical HAL output is YUV (e.g., YV12). Surface preview often expects RGB (commonly ABGR/RGBA via GraphicBuffer). Convert YV12 -> I420 -> ABGR using libyuv.
// YV12 (YYYY... VV... UU...) -> I420 (YYYY... UU... VV...)
static inline void convertYV12ToI420(const uint8_t* yv12, uint8_t* i420, int w, int h) {
const size_t ySize = static_cast<size_t>(w) * h;
const size_t cSize = ySize / 4;
// Y plane
memcpy(i420, yv12, ySize);
// V then U in YV12; swap to U then V for I420
memcpy(i420 + ySize, yv12 + ySize + cSize, cSize); // U
memcpy(i420 + ySize + cSize, yv12 + ySize, cSize); // V
}
Then with libyuv:
- I420ToABGR(I420_y, yStride, I420_u, uStride, I420_v, vStride, dst, dstStride, width, height)
Send the converted buffer to the output stream buffers and report via process_capture_result.
3. Routing same-ID opens to virtual devices in CameraService
Multiple apps may attempt to open the same logical camera ID. Preserve the public API (e.g., Camera.open(0)) but route subsequent opens to virtual devices internally. In CameraService::connectHelper, before handleEvictionsLocked, detect an already-active client with that cameraId; then rewrite the ID to the first available virtual device (e.g., 2, 3, ...).
// Pseudocode inside connectHelper before conflict checks
auto inUse = mActiveClientManager.get(cameraId);
if (inUse != nullptr) {
// Map to the first free virtual camera ID starting at 2
for (int vid = 2; ; ++vid) {
auto v = mActiveClientManager.get(std::to_string(vid));
if (v == nullptr) {
cameraId = std::to_string(vid);
break;
}
}
}
On MTK with rear/front physical devices only (IDs 0, 1 via HAL1) and a separate HAL3-based virtual module, virtual IDs can start at 2 and increment as needed.
Each new client is registered via finishConnectLocked -> mActiveClientManager.addAndEvict(...), which is how active occupancy is tracked.
4. Reference counting to keep the physical device alive
If the "main" app (the one actually driving the physical device) calls stopPreview() or disconnect(), the pipeline collapses unless we defer closing until all virtual clients are gone. Maintain integer reference counters using system properties (simple and SoC-friendly) for both the real devices and the virtual pool.
Examples on MTK (HAL1 driver entry at vendor/mediatek/.../device/1.x/device/CameraDevice1Base.cpp):
Real camera open increments its ref marker to 1 (can only be 0 or 1 per physical device):
// In CameraDevice1Base::open(...)
if (mInstanceId == 0) {
property_set("persist.camera0.ref", "1");
} else if (mInstanceId == 1) {
property_set("persist.camera1.ref", "1");
}
Real camera close resets to 0:
// In CameraDevice1Base::close()
if (mInstanceId == 0) {
property_set("persist.camera0.ref", "0");
} else if (mInstanceId == 1) {
property_set("persist.camera1.ref", "0");
}
Virtual camera open increments a shared counter:
// In virtual Camera::open(hw_device_t**)
char buf[PROPERTY_VALUE_MAX] = {};
property_get("persist.virtual_cam.ref", buf, "0");
int n = atoi(buf);
++n;
snprintf(buf, sizeof(buf), "%d", n);
property_set("persist.virtual_cam.ref", buf);
Do not decrement virtual_cam.ref in the virtual close path directly; instead, decrement at the framework client teardown point to centralize release decisions.
Intercept stopPreview and disconnect in CameraClient
When an app operates the physical devices (IDs 0 or 1), block stopPreview() and disconnect() if virtual clients exist. This keeps the hardware streaming while virtuals are attached.
// frameworks/av/services/camera/libcameraservice/api1/CameraClient.cpp
void CameraClient::stopPreview() {
LOG1("stopPreview (pid %d), mCameraId=%d", getCallingPid(), mCameraId);
Mutex::Autolock _l(mLock);
// Only guard for physical cameras
if (mCameraId != 2 && mCameraId != 3) {
char buf[PROPERTY_VALUE_MAX] = {};
property_get("persist.virtual_cam.ref", buf, "0");
if (atoi(buf) > 0) {
ALOGW("Virtual clients present, suppressing stopPreview on physical camera");
return;
}
}
if (checkPidAndHardware() != NO_ERROR) return;
disableMsgType(CAMERA_MSG_PREVIEW_FRAME);
mHardware->stopPreview();
sCameraService->updateProxyDeviceState(
hardware::ICameraServiceProxy::CAMERA_STATE_IDLE,
mCameraIdStr, mCameraFacing, mClientPackageName);
mPreviewBuffer.clear();
}
On disconnect(), if virtual clients exist, do not tear down the physical pipeline; instead, mark the physical ref as "pending close" (-1) and keep streaming.
binder::Status CameraClient::disconnect() {
int callingPid = getCallingPid();
Mutex::Autolock _l(mLock);
binder::Status ok = binder::Status::ok();
if (mCameraId != 2 && mCameraId != 3) {
char cam0[PROPERTY_VALUE_MAX] = {}, cam1[PROPERTY_VALUE_MAX] = {}, vref[PROPERTY_VALUE_MAX] = {};
property_get("persist.camera0.ref", cam0, "0");
property_get("persist.camera1.ref", cam1, "0");
property_get("persist.virtual_cam.ref", vref, "0");
if (atoi(vref) > 0) {
if (mCameraId == 0) property_set("persist.camera0.ref", "-1");
else if (mCameraId == 1) property_set("persist.camera1.ref", "-1");
// Keep HAL alive by attaching a new internal Surface (see next section)
setHeadlessPreviewWindow();
return ok; // Defer real disconnect until virtual ref hits 0
}
}
if (mHardware == 0) return ok;
disableMsgType(CAMERA_MSG_ALL_MSGS);
mHardware->stopPreview();
sCameraService->updateProxyDeviceState(
hardware::ICameraServiceProxy::CAMERA_STATE_IDLE,
mCameraIdStr, mCameraFacing, mClientPackageName);
mHardware->cancelPicture();
mHardware->release();
if (mPreviewWindow != 0) {
disconnectWindow(mPreviewWindow);
mPreviewWindow = 0;
mHardware->setPreviewWindow(mPreviewWindow);
}
mHardware.clear();
CameraService::Client::disconnect();
return ok;
}
Decrement virtual ref and close the physical device when last virtual leaves
BasicClient::disconnect() is invoked when a client dies. Use this hook to decrement the virtual ref count and, if zero and a physical device was marked as pending close (-1), close it now.
// frameworks/av/services/camera/libcameraservice/CameraService.cpp
binder::Status CameraService::BasicClient::disconnect() {
binder::Status st = Status::ok();
if (mDisconnected) return st;
mDisconnected = true;
sCameraService->removeByClient(this);
sCameraService->logDisconnected(mCameraIdStr, mClientPid, String8(mClientPackageName));
sp<IBinder> remote = getRemote();
if (remote != nullptr) remote->unlinkToDeath(sCameraService);
finishCameraOps();
sCameraService->mFlashlight->deviceClosed(mCameraIdStr);
mClientPid = 0;
const int id = cameraIdToInt(mCameraIdStr);
if (id >= 2) { // virtual camera
char vref[PROPERTY_VALUE_MAX] = {};
property_get("persist.virtual_cam.ref", vref, "0");
int n = atoi(vref);
n = (n > 0) ? n - 1 : 0;
char out[PROPERTY_VALUE_MAX] = {};
snprintf(out, sizeof(out), "%d", n);
property_set("persist.virtual_cam.ref", out);
if (n == 0) {
// Resolve any pending physical closes
char c0[PROPERTY_VALUE_MAX] = {}, c1[PROPERTY_VALUE_MAX] = {};
property_get("persist.camera0.ref", c0, "0");
property_get("persist.camera1.ref", c1, "0");
sp<CameraService::BasicClient> phys;
if (atoi(c0) == -1) {
phys = sCameraService->mActiveClientManager.getCameraClient(String8("0"));
} else if (atoi(c1) == -1) {
phys = sCameraService->mActiveClientManager.getCameraClient(String8("1"));
}
if (phys != nullptr) phys->disconnect();
}
}
return st;
}
5. Prevent BufferQueue abandonment when the main app removes its Surface
If the original app tears down its Surface (e.g., WindowManager.removeView), the HAL may fail dequeue_buffer/queue_buffer calls with "BufferQueue has been abandoned," causing previews in virtual clients to freeze or go black.
Attach a private, internal Surface to keep the HAL’s preview stream alive even after the UI Surface is gone. Create a new BufferQueue + GLConsumer and call setPreviewWindow with it.
// frameworks/av/services/camera/libcameraservice/api1/CameraClient.cpp
void CameraClient::setHeadlessPreviewWindow() {
ALOGI("setHeadlessPreviewWindow: creating internal Surface");
sp<IGraphicBufferProducer> producer;
sp<IGraphicBufferConsumer> consumer;
BufferQueue::createBufferQueue(&producer, &consumer);
GLuint tex = 0;
glGenTextures(1, &tex);
sp<GLConsumer> glc = new GLConsumer(consumer, tex, GL_TEXTURE_EXTERNAL_OES, /*useFence*/ true, /*isDetached*/ true);
if (glc == nullptr) {
ALOGE("Failed to create GLConsumer");
return;
}
glc->setName(String8::format("VCam-Headless-%d-%d", getpid(), createProcessUniqueId()));
glc->setDefaultBufferSize(1280, 720);
bool async = false;
int32_t usage = 0;
if (producer->query(NATIVE_WINDOW_CONSUMER_USAGE_BITS, &usage) == OK) {
if (usage & GraphicBuffer::USAGE_HW_TEXTURE) async = true;
}
sp<Surface> surface = new Surface(producer, async);
sp<IBinder> binder = IInterface::asBinder(producer);
setPreviewWindow(binder, surface);
}
By keeping a valid ANativeWindow atttached, the physical HAL continues to dequeue/enqueue buffers and fill shared memory for virtual readers.
6. End-to-end flow
- App A opens camera 0. The physical HAL1 device streams frames and writes every frame to shared memory.
- App B opens camera 0. CameraService detects camera 0 is busy and reroutes to virtual camera 2.
- Virtual camera (HAL3) serves App B by reading shared memory, converting YUV to RGB as needed, and returning buffers via process_capture_result.
- If App A calls stopPreview()/disconnect(), framework checks persist.virtual_cam.ref and defers shutting down the physical pipeline; it installs a headless Surface to prevent BufferQueue abandonment.
- When the final virtual cliant disconnects, BasicClient decrements the virtual ref and, if a physical close was pending, disconnects the real camera.
7. Implementation checklist
-
Framework
- Raise ClientManager limit or bypass mActiveClientManager conflicts as needed.
- In connectHelper, remap additional opens of the same physical ID to virtual IDs (2, 3, ...).
- Intercept CameraClient::stopPreview/ disconnect for physical devices to honor virtual refcount.
- Keep a headless preview Surface alive when the UI surface is removed.
-
HAL
- Physical HAL: copy each output frame in to shared memory from the display/preview path.
- Virtual HAL (HAL3): implement open/initialize/configure/ processCaptureRequest; read from shared memory and perform pixel format conversions.
-
IPC/shared memory
- Provide a HIDL/Binder service (or vendor-specific enterface) to back shared memory and synchronization between physical writer and virtual readers.
-
Format conversion
- YV12 -> I420 -> ABGR via libyuv; ensure correct strides and buffer alignment for GraphicBuffer.
-
Reference counting
- persist.camera0.ref / persist.camera1.ref: {0, 1, -1}
- persist.virtual_cam.ref: integer count of all virtual clients
- Resolve pending physical close when virtual count returns to 0.