diff --git a/bin/dbus/cx.ring.Ring.VideoManager.xml b/bin/dbus/cx.ring.Ring.VideoManager.xml
index 99dddaaffdff82c3687a95be2b8d9c31654527ea..a866cf4ee12a6ec43e2c4b02e5eee3a490540555 100644
--- a/bin/dbus/cx.ring.Ring.VideoManager.xml
+++ b/bin/dbus/cx.ring.Ring.VideoManager.xml
@@ -49,9 +49,9 @@
             </arg>
         </method>
 
-        <method name="startLocalRecorder" tp:name-for-bindings="startLocalRecorder">
-            <tp:docstring> Starts a local recorder. Video and/or audio are recorded from preferred devices.</tp:docstring>
-            <arg type="b" name="audioOnly" direction="in">
+        <method name="startLocalMediaRecorder" tp:name-for-bindings="startLocalMediaRecorder">
+            <tp:docstring> Starts a local recorder. Video and/or audio.</tp:docstring>
+            <arg type="s" name="videoInputId" direction="in">
             </arg>
             <arg type="s" name="filepath" direction="in">
             <tp:docstring> Base file path for local recording. This is not the final path as the file extension will be added (final path can be obtained as return value of this method).</tp:docstring>
@@ -63,17 +63,10 @@
 
         <method name="stopLocalRecorder" tp:name-for-bindings="stopLocalRecorder">
             <arg type="s" name="filepath" direction="in">
-            <tp:docstring> Identifier for local recorder to be stopped (obtained as return value of startLocalRecorder).</tp:docstring>
+            <tp:docstring> Identifier for local recorder to be stopped (obtained as return value of startLocalMediaRecorder).</tp:docstring>
             </arg>
         </method>
 
-        <method name="startCamera" tp:name-for-bindings="startCamera">
-            <tp:docstring> Starts the video camera, which renders the active v4l2 device's video to shared memory. Useful for testing/debugging camera settings</tp:docstring>
-        </method>
-
-        <method name="stopCamera" tp:name-for-bindings="stopCamera">
-        </method>
-
         <method name="startAudioDevice" tp:name-for-bindings="startAudioDevice">
             <tp:docstring> Starts the audio layer stream, so the audio device can be read.</tp:docstring>
         </method>
@@ -81,8 +74,9 @@
         <method name="stopAudioDevice" tp:name-for-bindings="stopAudioDevice">
         </method>
 
-        <method name="switchInput" tp:name-for-bindings="switchInput">
-            <arg type="s" name="resource" direction="in">
+        <method name="openVideoInput" tp:name-for-bindings="openVideoInput">
+            <tp:docstring>Open a video input by URI. A Sink is made available.</tp:docstring>
+            <arg type="s" name="inputUri" direction="in">
                 <tp:docstring>
                     A media resource locator (MRL).
                     Currently, the following are supported:
@@ -93,8 +87,18 @@
                     </ul>
                 </tp:docstring>
             </arg>
-            <arg type="b" name="switched" direction="out">
-                <tp:docstring>Returns true if the input stream was successfully changed, false otherwise</tp:docstring>
+            <arg type="s" name="inputId" direction="out">
+            <tp:docstring>The ID of the input. A sink with this ID is also made available.</tp:docstring>
+            </arg>
+        </method>
+
+        <method name="closeVideoInput" tp:name-for-bindings="closeVideoInput">
+            <tp:docstring>Closes a video input by ID</tp:docstring>
+            <arg type="s" name="inputId" direction="in">
+            <tp:docstring>The ID of the input to close, as returned by openVideoInput.</tp:docstring>
+            </arg>
+            <arg type="b" name="closed" direction="out">
+            <tp:docstring>Returns true if the input was closed</tp:docstring>
             </arg>
         </method>
 
diff --git a/bin/dbus/dbusvideomanager.cpp b/bin/dbus/dbusvideomanager.cpp
index 2218ad9cadc265398dbf1bd172c48de94d31efce..89c8543499cac6ef4d1d80b09e259d01178dcf5f 100644
--- a/bin/dbus/dbusvideomanager.cpp
+++ b/bin/dbus/dbusvideomanager.cpp
@@ -61,18 +61,6 @@ DBusVideoManager::getDefaultDevice() -> decltype(DRing::getDefaultDevice())
     return DRing::getDefaultDevice();
 }
 
-void
-DBusVideoManager::startCamera()
-{
-    DRing::startCamera();
-}
-
-void
-DBusVideoManager::stopCamera()
-{
-    DRing::stopCamera();
-}
-
 void
 DBusVideoManager::startAudioDevice()
 {
@@ -85,10 +73,14 @@ DBusVideoManager::stopAudioDevice()
     DRing::stopAudioDevice();
 }
 
-auto
-DBusVideoManager::switchInput(const std::string& resource) -> decltype(DRing::switchInput(resource))
-{
-    return DRing::switchInput(resource);
+std::string
+DBusVideoManager::openVideoInput(const std::string& inputUri)  {
+    return DRing::openVideoInput(inputUri);
+}
+
+bool
+DBusVideoManager::closeVideoInput(const std::string& inputId) {
+    return DRing::closeVideoInput(inputId);
 }
 
 auto
@@ -128,9 +120,9 @@ DBusVideoManager::getRenderer(const std::string& callId)
 }
 
 std::string
-DBusVideoManager::startLocalRecorder(const bool& audioOnly, const std::string& filepath)
+DBusVideoManager::startLocalMediaRecorder(const std::string& videoInputId, const std::string& filepath)
 {
-    return DRing::startLocalRecorder(audioOnly, filepath);
+    return DRing::startLocalMediaRecorder(videoInputId, filepath);
 }
 
 void
diff --git a/bin/dbus/dbusvideomanager.h b/bin/dbus/dbusvideomanager.h
index 3de30452fa156170e69fb7049c32aea575207842..78fb20c8b6d72eba37ee96f5034727908cdbe579 100644
--- a/bin/dbus/dbusvideomanager.h
+++ b/bin/dbus/dbusvideomanager.h
@@ -57,19 +57,18 @@ class DRING_PUBLIC DBusVideoManager :
         void applySettings(const std::string& deviceId, const std::map<std::string, std::string>& settings);
         void setDefaultDevice(const std::string& deviceId);
         std::string getDefaultDevice();
-        void startCamera();
-        void stopCamera();
         void startAudioDevice();
         void stopAudioDevice();
-        bool switchInput(const std::string& resource);
         bool getDecodingAccelerated();
         void setDecodingAccelerated(const bool& state);
         bool getEncodingAccelerated();
         void setEncodingAccelerated(const bool& state);
         void setDeviceOrientation(const std::string& deviceId, const int& angle);
         std::map<std::string, std::string> getRenderer(const std::string& callId);
-        std::string startLocalRecorder(const bool& audioOnly, const std::string& filepath);
+        std::string startLocalMediaRecorder(const std::string& videoInputId, const std::string& filepath);
         void stopLocalRecorder(const std::string& filepath);
+        std::string openVideoInput(const std::string& inputUri);
+        bool closeVideoInput(const std::string& inputId);
 };
 
 #endif // __RING_DBUSVIDEOMANAGER_H__
diff --git a/bin/jni/videomanager.i b/bin/jni/videomanager.i
index 5f853489cbe50221ada2f6d6f8e822aa2152be9e..71f4ee589c0a9115d9c97c1008f201076fc45bcc 100644
--- a/bin/jni/videomanager.i
+++ b/bin/jni/videomanager.i
@@ -149,10 +149,14 @@ int AndroidFormatToAVFormat(int androidformat) {
     }
 }
 
-JNIEXPORT void JNICALL Java_net_jami_daemon_JamiServiceJNI_captureVideoPacket(JNIEnv *jenv, jclass jcls, jobject buffer, jint size, jint offset, jboolean keyframe, jlong timestamp, jint rotation)
+JNIEXPORT void JNICALL Java_net_jami_daemon_JamiServiceJNI_captureVideoPacket(JNIEnv *jenv, jclass jcls, jstring inputId, jobject buffer, jint size, jint offset, jboolean keyframe, jlong timestamp, jint rotation)
 {
     try {
-        auto frame = DRing::getNewFrame();
+        const char *inputId_pstr = (const char *)jenv->GetStringUTFChars(inputId, 0);
+        if (!inputId_pstr)
+            return;
+        std::string_view input(inputId_pstr);
+        auto frame = DRing::getNewFrame(input);
         if (not frame)
             return;
         auto packet = std::unique_ptr<AVPacket, void(*)(AVPacket*)>(new AVPacket, [](AVPacket* pkt){
@@ -174,13 +178,13 @@ JNIEXPORT void JNICALL Java_net_jami_daemon_JamiServiceJNI_captureVideoPacket(JN
         packet->size = size;
         packet->pts = timestamp;
         frame->setPacket(std::move(packet));
-        DRing::publishFrame();
+        DRing::publishFrame(input);
     } catch (const std::exception& e) {
         __android_log_print(ANDROID_LOG_ERROR, TAG, "Exception capturing video packet: %s", e.what());
     }
 }
 
-JNIEXPORT void JNICALL Java_net_jami_daemon_JamiServiceJNI_captureVideoFrame(JNIEnv *jenv, jclass jcls, jobject image, jint rotation)
+JNIEXPORT void JNICALL Java_net_jami_daemon_JamiServiceJNI_captureVideoFrame(JNIEnv *jenv, jclass jcls, jstring inputId, jobject image, jint rotation)
 {
     static jclass imageClass = jenv->GetObjectClass(image);
     static jmethodID imageGetFormat = jenv->GetMethodID(imageClass, "getFormat", "()I");
@@ -189,9 +193,18 @@ JNIEXPORT void JNICALL Java_net_jami_daemon_JamiServiceJNI_captureVideoFrame(JNI
     static jmethodID imageGetCropRect = jenv->GetMethodID(imageClass, "getCropRect", "()Landroid/graphics/Rect;");
     static jmethodID imageGetPlanes = jenv->GetMethodID(imageClass, "getPlanes", "()[Landroid/media/Image$Plane;");
     static jmethodID imageClose = jenv->GetMethodID(imageClass, "close", "()V");
+    if(!inputId) {
+        SWIG_JavaThrowException(jenv, SWIG_JavaNullPointerException, "null string");
+        return;
+    }
 
     try {
-        auto frame = DRing::getNewFrame();
+        const char *inputId_pstr = (const char *)jenv->GetStringUTFChars(inputId, 0);
+        if (!inputId_pstr)
+            return;
+        std::string_view input(inputId_pstr);
+
+        auto frame = DRing::getNewFrame(input);
         if (not frame) {
             jenv->CallVoidMethod(image, imageClose);
             return;
@@ -277,7 +290,8 @@ JNIEXPORT void JNICALL Java_net_jami_daemon_JamiServiceJNI_captureVideoFrame(JNI
             if (justAttached)
                 gJavaVM->DetachCurrentThread();
         });
-        DRing::publishFrame();
+        DRing::publishFrame(input);
+        jenv->ReleaseStringUTFChars(inputId, inputId_pstr);
     } catch (const std::exception& e) {
         __android_log_print(ANDROID_LOG_ERROR, TAG, "Exception capturing video frame: %s", e.what());
     }
@@ -352,7 +366,7 @@ JNIEXPORT void JNICALL Java_net_jami_daemon_JamiServiceJNI_registerVideoCallback
     const char *arg1_pstr = (const char *)jenv->GetStringUTFChars(sinkId, 0);
     if (!arg1_pstr)
         return;
-    const std::string sink(arg1_pstr);
+    std::string sink(arg1_pstr);
     jenv->ReleaseStringUTFChars(sinkId, arg1_pstr);
 
     ANativeWindow* nativeWindow = (ANativeWindow*)((intptr_t) window);
@@ -395,19 +409,16 @@ JNIEXPORT void JNICALL Java_net_jami_daemon_JamiServiceJNI_unregisterVideoCallba
 %native(registerVideoCallback) void registerVideoCallback(jstring, jlong);
 %native(unregisterVideoCallback) void unregisterVideoCallback(jstring, jlong);
 
-%native(captureVideoFrame) void captureVideoFrame(jobject, jint);
-%native(captureVideoPacket) void captureVideoPacket(jobject, jint, jint, jboolean, jlong, jint);
+%native(captureVideoFrame) void captureVideoFrame(jstring, jobject, jint);
+%native(captureVideoPacket) void captureVideoPacket(jstring, jobject, jint, jint, jboolean, jlong, jint);
 
 namespace DRing {
 
 void setDefaultDevice(const std::string& name);
 std::string getDefaultDevice();
 
-void startCamera();
-void stopCamera();
 void startAudioDevice();
 void stopAudioDevice();
-bool switchInput(const std::string& resource);
 std::map<std::string, std::string> getSettings(const std::string& name);
 void applySettings(const std::string& name, const std::map<std::string, std::string>& settings);
 
@@ -415,12 +426,15 @@ void addVideoDevice(const std::string &node);
 void removeVideoDevice(const std::string &node);
 void setDeviceOrientation(const std::string& name, int angle);
 void registerSinkTarget(const std::string& sinkId, const DRing::SinkTarget& target);
-std::string startLocalRecorder(const bool& audioOnly, const std::string& filepath);
+std::string startLocalMediaRecorder(const std::string& videoInputId, const std::string& filepath);
 void stopLocalRecorder(const std::string& filepath);
 bool getDecodingAccelerated();
 void setDecodingAccelerated(bool state);
 bool getEncodingAccelerated();
 void setEncodingAccelerated(bool state);
+
+std::string openVideoInput(const std::string& path);
+bool closeVideoInput(const std::string& id);
 }
 
 class VideoCallback {
diff --git a/bin/nodejs/videomanager.i b/bin/nodejs/videomanager.i
index 7cce141216a565748bda37926175d66bd6c37b46..3d55c272b96da7b5c7952a87651e87b70c4a5153 100644
--- a/bin/nodejs/videomanager.i
+++ b/bin/nodejs/videomanager.i
@@ -46,11 +46,8 @@ namespace DRing {
 void setDefaultDevice(const std::string& name);
 std::string getDefaultDevice();
 
-void startCamera();
-void stopCamera();
 void startAudioDevice();
 void stopAudioDevice();
-bool switchInput(const std::string& resource);
 std::map<std::string, std::string> getSettings(const std::string& name);
 void applySettings(const std::string& name, const std::map<std::string, std::string>& settings);
 
diff --git a/src/client/videomanager.cpp b/src/client/videomanager.cpp
index 3e230ebb3aa27918708f45c9daf193014dd08532..91980fb791e4176461cb5a518a17605ad4e15880 100644
--- a/src/client/videomanager.cpp
+++ b/src/client/videomanager.cpp
@@ -350,7 +350,8 @@ VideoFrame::getOrientation() const
 {
     int32_t* matrix {nullptr};
     if (auto p = packet()) {
-        matrix = reinterpret_cast<int32_t*>(av_packet_get_side_data(p, AV_PKT_DATA_DISPLAYMATRIX, nullptr));
+        matrix = reinterpret_cast<int32_t*>(
+            av_packet_get_side_data(p, AV_PKT_DATA_DISPLAYMATRIX, nullptr));
     } else if (auto p = pointer()) {
         if (AVFrameSideData* side_data = av_frame_get_side_data(p, AV_FRAME_DATA_DISPLAYMATRIX)) {
             matrix = reinterpret_cast<int32_t*>(side_data->data);
@@ -358,24 +359,26 @@ VideoFrame::getOrientation() const
     }
     if (matrix) {
         double angle = av_display_rotation_get(matrix);
-        return std::isnan(angle) ? 0 : -(int)angle;
+        return std::isnan(angle) ? 0 : -(int) angle;
     }
     return 0;
 }
 
 VideoFrame*
-getNewFrame()
+getNewFrame(std::string_view id)
 {
-    if (auto input = jami::Manager::instance().getVideoManager().videoInput.lock())
+    if (auto input = jami::Manager::instance().getVideoManager().getVideoInput(id))
         return &input->getNewFrame();
+    JAMI_WARN("getNewFrame: can't find input %.*s", (int) id.size(), id.data());
     return nullptr;
 }
 
 void
-publishFrame()
+publishFrame(std::string_view id)
 {
-    if (auto input = jami::Manager::instance().getVideoManager().videoInput.lock())
+    if (auto input = jami::Manager::instance().getVideoManager().getVideoInput(id))
         return input->publishFrame();
+    JAMI_WARN("publishFrame: can't find input %.*s", (int) id.size(), id.data());
 }
 
 void
@@ -446,57 +449,58 @@ applySettings(const std::string& deviceId, const std::map<std::string, std::stri
 }
 
 void
-startCamera()
+startAudioDevice()
 {
-    jami::Manager::instance().getVideoManager().videoPreview = jami::getVideoCamera();
-    jami::Manager::instance().getVideoManager().started = switchInput(
-        jami::Manager::instance().getVideoManager().videoDeviceMonitor.getMRLForDefaultDevice());
+    auto newPreview = jami::getAudioInput(jami::RingBufferPool::DEFAULT_ID);
+    jami::Manager::instance().getVideoManager().audioPreview = newPreview;
+    newPreview->switchInput("");
 }
 
 void
-stopCamera()
+stopAudioDevice()
 {
-    jami::Manager::instance().getVideoManager().started = false;
-    jami::Manager::instance().getVideoManager().videoPreview.reset();
+    jami::Manager::instance().getVideoManager().audioPreview.reset();
 }
 
-void
-startAudioDevice()
+std::string
+openVideoInput(const std::string& path)
 {
-    jami::Manager::instance().getVideoManager().audioPreview = jami::getAudioInput(
-        jami::RingBufferPool::DEFAULT_ID);
-    jami::Manager::instance().getVideoManager().audioPreview->switchInput("");
+    auto& vm = jami::Manager::instance().getVideoManager();
+
+    auto id = path.empty() ? vm.videoDeviceMonitor.getMRLForDefaultDevice() : path;
+    auto& input = vm.clientVideoInputs[id];
+    if (not input) {
+        input = jami::getVideoInput(id);
+    }
+    return id;
 }
 
-void
-stopAudioDevice()
+bool
+closeVideoInput(const std::string& id)
 {
-    jami::Manager::instance().getVideoManager().audioPreview.reset();
+    return jami::Manager::instance().getVideoManager().clientVideoInputs.erase(id) > 0;
 }
 
 std::string
-startLocalRecorder(const bool& audioOnly, const std::string& filepath)
+startLocalMediaRecorder(const std::string& videoInputId, const std::string& filepath)
 {
-    if (!audioOnly && !jami::Manager::instance().getVideoManager().started) {
-        JAMI_ERR("Couldn't start local video recorder (camera is not started)");
-        return "";
-    }
-
-    auto rec = std::make_unique<jami::LocalRecorder>(audioOnly);
+    auto rec = std::make_unique<jami::LocalRecorder>(videoInputId);
     rec->setPath(filepath);
 
     // retrieve final path (containing file extension)
     auto path = rec->getPath();
 
+    auto& recordManager = jami::LocalRecorderManager::instance();
+
     try {
-        jami::LocalRecorderManager::instance().insertRecorder(path, std::move(rec));
+        recordManager.insertRecorder(path, std::move(rec));
     } catch (const std::invalid_argument&) {
         return "";
     }
 
-    auto ret = jami::LocalRecorderManager::instance().getRecorderByPath(path)->startRecording();
+    auto ret = recordManager.getRecorderByPath(path)->startRecording();
     if (!ret) {
-        jami::LocalRecorderManager::instance().removeRecorderByPath(filepath);
+        recordManager.removeRecorderByPath(filepath);
         return "";
     }
 
@@ -516,20 +520,6 @@ stopLocalRecorder(const std::string& filepath)
     jami::LocalRecorderManager::instance().removeRecorderByPath(filepath);
 }
 
-bool
-switchInput(const std::string& resource)
-{
-    bool ret = true;
-    if (auto input = jami::Manager::instance().getVideoManager().videoInput.lock())
-        ret = input->switchInput(resource).valid();
-    else
-        JAMI_WARN("Video input not initialized");
-
-    if (auto input = jami::Manager::instance().getVideoManager().audioPreview)
-        ret &= input->switchInput(resource).valid();
-    return ret;
-}
-
 void
 registerSinkTarget(const std::string& sinkId, const SinkTarget& target)
 {
@@ -673,19 +663,6 @@ removeVideoDevice(const std::string& node)
 
 namespace jami {
 
-std::shared_ptr<video::VideoFrameActiveWriter>
-getVideoCamera()
-{
-    auto& vmgr = Manager::instance().getVideoManager();
-    if (auto input = vmgr.videoInput.lock())
-        return input;
-
-    vmgr.started = false;
-    auto input = std::make_shared<video::VideoInput>();
-    vmgr.videoInput = input;
-    return input;
-}
-
 video::VideoDeviceMonitor&
 getVideoDeviceMonitor()
 {
diff --git a/src/client/videomanager.h b/src/client/videomanager.h
index d26e4a18f7c091ae3436ccd0bdc22e60e664c16d..04210fea817a769d5a1cf01a52b4ed92f13a1b18 100644
--- a/src/client/videomanager.h
+++ b/src/client/videomanager.h
@@ -16,8 +16,7 @@
  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
  */
 
-#ifndef VIDEOMANAGER_H_
-#define VIDEOMANAGER_H_
+#pragma once
 
 #ifdef HAVE_CONFIG_H
 #include "config.h"
@@ -42,37 +41,31 @@ struct VideoManager
 public:
     void setDeviceOrientation(const std::string& deviceId, int angle);
 
-    /**
-     * VideoManager acts as a cache of the active VideoInput.
-     * When this input is needed, you must use getVideoCamera
-     * to create the instance if not done yet and obtain a shared pointer
-     * for your own usage.
-     * VideoManager instance doesn't increment the reference count of
-     * this video input instance: this instance is destroyed when the last
-     * external user has released its shared pointer.
-     */
-    std::weak_ptr<video::VideoInput> videoInput;
-    std::shared_ptr<video::VideoFrameActiveWriter> videoPreview;
+    // Client-managed video inputs and players
+    std::map<std::string, std::shared_ptr<video::VideoInput>> clientVideoInputs;
+    std::map<std::string, std::shared_ptr<MediaPlayer>> mediaPlayers;
+    // Client-managed audio preview
+    std::shared_ptr<AudioInput> audioPreview;
+
+    // device monitor
     video::VideoDeviceMonitor videoDeviceMonitor;
-    std::atomic_bool started;
+
     /**
-     * VideoManager also acts as a cache of the active AudioInput(s).
-     * When one of these is needed, you must use getAudioInput, which will
-     * create an instance if need be and return a shared_ptr.
+     * Cache of the active Audio/Video input(s).
      */
-    std::map<std::string, std::weak_ptr<AudioInput>> audioInputs;
-    std::map<std::string, std::weak_ptr<video::VideoInput>> videoInputs;
-    std::map<std::string, std::shared_ptr<MediaPlayer>> mediaPlayers;
+    std::map<std::string, std::weak_ptr<AudioInput>, std::less<>> audioInputs;
+    std::map<std::string, std::weak_ptr<video::VideoInput>, std::less<>> videoInputs;
     std::mutex audioMutex;
-    std::shared_ptr<AudioInput> audioPreview;
     bool hasRunningPlayers();
+    std::shared_ptr<video::VideoInput> getVideoInput(std::string_view id) const {
+        auto input = videoInputs.find(id);
+        return input == videoInputs.end() ? nullptr : input->second.lock();
+    }
 };
 
-std::shared_ptr<video::VideoFrameActiveWriter> getVideoCamera();
 video::VideoDeviceMonitor& getVideoDeviceMonitor();
 std::shared_ptr<AudioInput> getAudioInput(const std::string& id);
-std::shared_ptr<video::VideoInput> getVideoInput(
-    const std::string& id, video::VideoInputMode inputMode = video::VideoInputMode::Undefined);
+std::shared_ptr<video::VideoInput> getVideoInput(const std::string& id, video::VideoInputMode inputMode = video::VideoInputMode::Undefined);
 std::string createMediaPlayer(const std::string& path);
 std::shared_ptr<MediaPlayer> getMediaPlayer(const std::string& id);
 bool pausePlayer(const std::string& id, bool pause);
@@ -82,5 +75,3 @@ bool playerSeekToTime(const std::string& id, int time);
 int64_t getPlayerPosition(const std::string& id);
 
 } // namespace jami
-
-#endif // VIDEOMANAGER_H_
diff --git a/src/conference.cpp b/src/conference.cpp
index fa8626f09de6add20e5f8d49c78ef2ba487e16e1..f6245fc91a3dce7498f83f54cf702ea03530b9e5 100644
--- a/src/conference.cpp
+++ b/src/conference.cpp
@@ -66,7 +66,7 @@ Conference::Conference(bool enableVideo)
     if (not videoEnabled_)
         return;
 
-    videoMixer_ = std::make_shared<video::VideoMixer>(id_);
+    videoMixer_ = std::make_shared<video::VideoMixer>(id_, mediaInput_);
     videoMixer_->setOnSourcesUpdated([this](std::vector<video::SourceInfo>&& infos) {
         runOnMainThread([w = weak(), infos = std::move(infos)] {
             auto shared = w.lock();
diff --git a/src/jami/videomanager_interface.h b/src/jami/videomanager_interface.h
index ff82e011e07b4c8a563768d04d1eb1a2b61eedd7..337c9d10dd3e1d8409ab26ecf4751f1f688c0eda 100644
--- a/src/jami/videomanager_interface.h
+++ b/src/jami/videomanager_interface.h
@@ -192,23 +192,24 @@ DRING_PUBLIC void setDefaultDevice(const std::string& deviceId);
 DRING_PUBLIC void setDeviceOrientation(const std::string& deviceId, int angle);
 DRING_PUBLIC std::map<std::string, std::string> getDeviceParams(const std::string& deviceId);
 DRING_PUBLIC std::string getDefaultDevice();
-DRING_PUBLIC void startCamera();
-DRING_PUBLIC void stopCamera();
 DRING_PUBLIC void startAudioDevice();
 DRING_PUBLIC void stopAudioDevice();
+
+DRING_PUBLIC std::string openVideoInput(const std::string& path);
+DRING_PUBLIC bool closeVideoInput(const std::string& id);
+
 DRING_PUBLIC std::string createMediaPlayer(const std::string& path);
+DRING_PUBLIC std::string closeMediaPlayer(const std::string& id);
 DRING_PUBLIC bool pausePlayer(const std::string& id, bool pause);
-DRING_PUBLIC bool closePlayer(const std::string& id);
 DRING_PUBLIC bool mutePlayerAudio(const std::string& id, bool mute);
 DRING_PUBLIC bool playerSeekToTime(const std::string& id, int time);
 int64_t getPlayerPosition(const std::string& id);
 
-DRING_PUBLIC bool switchInput(const std::string& resource);
 DRING_PUBLIC void registerSinkTarget(const std::string& sinkId, const SinkTarget& target);
 DRING_PUBLIC void registerAVSinkTarget(const std::string& sinkId, const AVSinkTarget& target);
 DRING_PUBLIC std::map<std::string, std::string> getRenderer(const std::string& callId);
 
-DRING_PUBLIC std::string startLocalRecorder(const bool& audioOnly, const std::string& filepath);
+DRING_PUBLIC std::string startLocalMediaRecorder(const std::string& videoInputId, const std::string& filepath);
 DRING_PUBLIC void stopLocalRecorder(const std::string& filepath);
 
 #if defined(__ANDROID__) || defined(RING_UWP) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS)
@@ -216,8 +217,8 @@ DRING_PUBLIC void addVideoDevice(
     const std::string& node,
     const std::vector<std::map<std::string, std::string>>& devInfo = {});
 DRING_PUBLIC void removeVideoDevice(const std::string& node);
-DRING_PUBLIC VideoFrame* getNewFrame();
-DRING_PUBLIC void publishFrame();
+DRING_PUBLIC VideoFrame* getNewFrame(std::string_view id);
+DRING_PUBLIC void publishFrame(std::string_view id);
 #endif
 
 DRING_PUBLIC bool getDecodingAccelerated();
diff --git a/src/media/localrecorder.cpp b/src/media/localrecorder.cpp
index 536ac7c5a08d36120fe1f48b84113e1b4929d833..a3b82c06f0d10f4da296758ca14a2bdc3a50701e 100644
--- a/src/media/localrecorder.cpp
+++ b/src/media/localrecorder.cpp
@@ -30,10 +30,11 @@
 
 namespace jami {
 
-LocalRecorder::LocalRecorder(const bool& audioOnly)
+LocalRecorder::LocalRecorder(const std::string& inputUri)
 {
-    isAudioOnly_ = audioOnly;
-    recorder_->audioOnly(audioOnly);
+    inputUri_ = inputUri;
+    isAudioOnly_ = inputUri_.empty();
+    recorder_->audioOnly(isAudioOnly_);
 }
 
 LocalRecorder::~LocalRecorder()
@@ -76,7 +77,7 @@ LocalRecorder::startRecording()
     // create read offset in RingBuffer
     Manager::instance().getRingBufferPool().bindHalfDuplexOut(path_, RingBufferPool::DEFAULT_ID);
 
-    audioInput_ = jami::getAudioInput(path_);
+    audioInput_ = getAudioInput(path_);
     audioInput_->setFormat(AudioFormat::STEREO());
     audioInput_->attach(recorder_->addStream(audioInput_->getInfo()));
     audioInput_->switchInput("");
@@ -84,7 +85,7 @@ LocalRecorder::startRecording()
 #ifdef ENABLE_VIDEO
     // video recording
     if (!isAudioOnly_) {
-        videoInput_ = std::static_pointer_cast<video::VideoInput>(jami::getVideoCamera());
+        videoInput_ = std::static_pointer_cast<video::VideoInput>(getVideoInput(inputUri_));
         if (videoInput_) {
             videoInput_->attach(recorder_->addStream(videoInput_->getInfo()));
         } else {
diff --git a/src/media/localrecorder.h b/src/media/localrecorder.h
index 1aa5370f34b824ba2a1238c1f78c9bc8641b9fdf..f4851a952b8cd1d0de61e8eecee4f1204c67e020 100644
--- a/src/media/localrecorder.h
+++ b/src/media/localrecorder.h
@@ -40,7 +40,7 @@ namespace jami {
 class LocalRecorder : public Recordable
 {
 public:
-    LocalRecorder(const bool& audioOnly);
+    LocalRecorder(const std::string& inputUri);
     ~LocalRecorder();
 
     /**
@@ -57,10 +57,11 @@ public:
     /**
      * Stops recording.
      */
-    void stopRecording();
+    void stopRecording() override;
 
 private:
     std::string path_;
+    std::string inputUri_;
 
     // media inputs
     std::shared_ptr<jami::video::VideoInput> videoInput_;
diff --git a/src/media/media_recorder.cpp b/src/media/media_recorder.cpp
index 2f13a1afabfc739751ecb97b683ee6220afd241f..415865af43342c055075b0bb546c420761d044f6 100644
--- a/src/media/media_recorder.cpp
+++ b/src/media/media_recorder.cpp
@@ -407,6 +407,10 @@ MediaRecorder::setupVideoOutput()
     int ret = -1;
     int streams = peer.isValid() + local.isValid() + mixer.isValid();
     switch (streams) {
+    case 0: {
+        JAMI_ERR("Trying to record a stream but none is valid");
+        break;
+    }
     case 1: {
         MediaStream inputStream;
         if (peer.isValid())
diff --git a/src/media/recordable.h b/src/media/recordable.h
index 5184f3f231a63b1c956b434c18ec8f2f97c83349..63c7432fa4b3fe534bd7bca1de615198cc70d9bb 100644
--- a/src/media/recordable.h
+++ b/src/media/recordable.h
@@ -55,7 +55,7 @@ public:
     /**
      * Stop recording
      */
-    void stopRecording();
+    virtual void stopRecording();
 
     /**
      * Start recording
diff --git a/src/media/video/video_input.cpp b/src/media/video/video_input.cpp
index f4af01384253b20017f342dab8d3c81139bb4809..aad46f712c5db23085838f30cb3f30558dc98149 100644
--- a/src/media/video/video_input.cpp
+++ b/src/media/video/video_input.cpp
@@ -76,6 +76,7 @@ VideoInput::VideoInput(VideoInputMode inputMode, const std::string& id_)
         sink_ = Manager::instance().createSinkClient(id_);
     }
 #endif
+    switchInput(id_);
 }
 
 VideoInput::~VideoInput()
@@ -576,12 +577,6 @@ VideoInput::switchInput(const std::string& resource)
     return futureDecOpts_;
 }
 
-const DeviceParams&
-VideoInput::getParams() const
-{
-    return decOpts_;
-}
-
 MediaStream
 VideoInput::getInfo() const
 {
diff --git a/src/media/video/video_input.h b/src/media/video/video_input.h
index d0d3f3207c94432722ec36177cd1305001bd1498..f63a997eb2a9a6fbf14dc5abf8908ba06a75a087 100644
--- a/src/media/video/video_input.h
+++ b/src/media/video/video_input.h
@@ -65,7 +65,14 @@ public:
     int getWidth() const;
     int getHeight() const;
     AVPixelFormat getPixelFormat() const;
-    const DeviceParams& getParams() const;
+
+    const DeviceParams& getConfig() const {
+        return decOpts_;
+    }
+    std::shared_future<DeviceParams> getParams() const {
+        return futureDecOpts_;
+    }
+    
     MediaStream getInfo() const;
 
     void setSink(const std::string& sinkId);
@@ -80,7 +87,6 @@ public:
     void setupSink();
     void stopSink();
 
-    std::shared_future<DeviceParams> switchInput(const std::string& resource);
 #if VIDEO_CLIENT_INPUT
     /*
      * these functions are used to pass buffer from/to the daemon
@@ -98,6 +104,8 @@ public:
 private:
     NON_COPYABLE(VideoInput);
 
+    std::shared_future<DeviceParams> switchInput(const std::string& resource);
+
     std::string id_;
     std::string currentResource_;
     std::atomic<bool> switchPending_ = {false};
diff --git a/src/media/video/video_mixer.cpp b/src/media/video/video_mixer.cpp
index 0540436d50e790615a876b3ab158750357ee1fbb..b8e030e412edc4e4c0773ac54b4fba30c242fc80 100644
--- a/src/media/video/video_mixer.cpp
+++ b/src/media/video/video_mixer.cpp
@@ -78,14 +78,15 @@ private:
 static constexpr const auto MIXER_FRAMERATE = 30;
 static constexpr const auto FRAME_DURATION = std::chrono::duration<double>(1. / MIXER_FRAMERATE);
 
-VideoMixer::VideoMixer(const std::string& id)
+VideoMixer::VideoMixer(const std::string& id, const std::string& localInput)
     : VideoGenerator::VideoGenerator()
     , id_(id)
     , sink_(Manager::instance().createSinkClient(id, true))
     , loop_([] { return true; }, std::bind(&VideoMixer::process, this), [] {})
 {
     // Local video camera is the main participant
-    videoLocal_ = getVideoCamera();
+    if (not localInput.empty())
+        videoLocal_ = getVideoInput(localInput);
     if (videoLocal_)
         videoLocal_->attach(this);
     loop_.start();
@@ -128,8 +129,6 @@ VideoMixer::switchInput(const std::string& input)
             localInput->stopInput();
         }
 #endif
-    } else {
-        videoLocal_ = getVideoCamera();
     }
 
     if (input.empty()) {
@@ -138,12 +137,9 @@ VideoMixer::switchInput(const std::string& input)
     }
 
     // Re-attach videoInput to mixer
-    if (videoLocal_) {
-        if (auto localInput = std::dynamic_pointer_cast<VideoInput>(videoLocal_)) {
-            localInput->switchInput(input);
-        }
+    videoLocal_ = getVideoInput(input);
+    if (videoLocal_)
         videoLocal_->attach(this);
-    }
 }
 
 void
@@ -159,7 +155,6 @@ VideoMixer::switchSecondaryInput(const std::string& input)
         }
 #endif
     }
-    videoLocalSecondary_ = getVideoInput(input);
 
     if (input.empty()) {
         JAMI_DBG("[mixer:%s] Input is empty, don't add it in the mixer", id_.c_str());
@@ -167,9 +162,8 @@ VideoMixer::switchSecondaryInput(const std::string& input)
     }
 
     // Re-attach videoInput to mixer
+    videoLocalSecondary_ = getVideoInput(input);
     if (videoLocalSecondary_) {
-        if (auto videoInput = std::dynamic_pointer_cast<VideoInput>(videoLocalSecondary_))
-            videoInput->switchInput(input);
         videoLocalSecondary_->attach(this);
     }
 }
diff --git a/src/media/video/video_mixer.h b/src/media/video/video_mixer.h
index 872674670b6dfddee03585b0c62a0b5832ba7238..bf2d56b60e6ff78685059ba3cb368ced5590ca53 100644
--- a/src/media/video/video_mixer.h
+++ b/src/media/video/video_mixer.h
@@ -53,7 +53,7 @@ enum class Layout { GRID, ONE_BIG_WITH_SMALL, ONE_BIG };
 class VideoMixer : public VideoGenerator, public VideoFramePassiveReader
 {
 public:
-    VideoMixer(const std::string& id);
+    VideoMixer(const std::string& id, const std::string& localInput = {});
     ~VideoMixer();
 
     void setParameters(int width, int height, AVPixelFormat format = AV_PIX_FMT_YUV422P);
diff --git a/src/media/video/video_rtp_session.cpp b/src/media/video/video_rtp_session.cpp
index de414386c260e0f084376f9123e6fad5e658be17..9e6667511b947b90adc7b5b2d59d09170624a6cd 100644
--- a/src/media/video/video_rtp_session.cpp
+++ b/src/media/video/video_rtp_session.cpp
@@ -112,11 +112,10 @@ VideoRtpSession::startSender()
         }
 
         if (not conference_) {
-            videoLocal_ = getVideoCamera();
-            if (auto input = Manager::instance().getVideoManager().videoInput.lock()) {
-                std::static_pointer_cast<VideoInput>(videoLocal_)
-                    ->setSuccessfulSetupCb(onSuccessfulSetup_);
-                auto newParams = input->switchInput(input_);
+            auto input = getVideoInput(input_);
+            videoLocal_ = input;
+            if (input) {
+                auto newParams = input->getParams();
                 try {
                     if (newParams.valid()
                         && newParams.wait_for(NEWPARAMS_TIMEOUT) == std::future_status::ready) {
@@ -584,7 +583,7 @@ VideoRtpSession::setNewBitrate(unsigned int newBR)
 
 #if __ANDROID__
         if (auto input_device = std::dynamic_pointer_cast<VideoInput>(videoLocal_))
-            emitSignal<DRing::VideoSignal::SetBitrate>(input_device->getParams().name, (int) newBR);
+            emitSignal<DRing::VideoSignal::SetBitrate>(input_device->getConfig().name, (int) newBR);
 #endif
 
         if (sender_) {
diff --git a/tools/jamictrl/jamictrl.py b/tools/jamictrl/jamictrl.py
index 78d3db3f92bda685d3181804a78be661785a7cad..0af5605d3c79569c4917f06bf1368af701255f1e 100755
--- a/tools/jamictrl/jamictrl.py
+++ b/tools/jamictrl/jamictrl.py
@@ -233,9 +233,9 @@ if __name__ == "__main__":
         import time
         while True:
             time.sleep(2)
-            ctrl.videomanager.startCamera()
+            id = ctrl.videomanager.openVideoInput("")
             time.sleep(2)
-            ctrl.videomanager.stopCamera()
+            ctrl.videomanager.closeVideoInput(id)
 
     if len(sys.argv) == 1 or ctrl.autoAnswer:
         signal.signal(signal.SIGINT, ctrl.interruptHandler)
diff --git a/tools/jamictrl/toggle_video_preview.py b/tools/jamictrl/toggle_video_preview.py
index 6e7f96a33a4228a4e882121cec0beefde5e539dd..1829e3f67fbf2c6ae5a98dd60f7cb46d3ca229dc 100755
--- a/tools/jamictrl/toggle_video_preview.py
+++ b/tools/jamictrl/toggle_video_preview.py
@@ -25,7 +25,6 @@ import sys
 import os
 from random import randint
 
-
 class DRingToggleVideo():
     def start(self):
         bus = dbus.SessionBus()
@@ -35,6 +34,6 @@ class DRingToggleVideo():
 
         while True:
             time.sleep(2)
-            videoControl.startCamera()
+            id = videoControl.openVideoInput("")
             time.sleep(2)
-            videoControl.stopCamera()
+            videoControl.closeVideoInput(id)