diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..f6afbad5f45404658981be735e46471fd2aa002a
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,23 @@
+FROM app.ftlab.utu.fi/base:1.8-dev
+
+ARG USERNAME=user
+ARG USER_UID=1000
+ARG USER_GID=$USER_UID
+
+# Create the user
+RUN addgroup --gid $USER_GID user \
+    && adduser --disabled-password --gecos '' --uid $USER_UID --gid $USER_GID user && usermod -a -G audio user && usermod -a -G video user \
+    #
+    # [Optional] Add sudo support. Omit if you don't need to install software after connecting.
+    && apt-get update \
+    && apt-get install -y sudo python3-pip file \
+    && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
+    && chmod 0440 /etc/sudoers.d/$USERNAME \
+    && pip install cpplint cppclean
+
+# ********************************************************
+# * Anything else you want to do like clean up goes here *
+# ********************************************************
+
+# [Optional] Set the default user. Omit if you want to keep the default as root.
+USER $USERNAME
\ No newline at end of file
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000000000000000000000000000000000000..bc61e307b342f3ac3f37cc4ce05bb1cd1c3e9eef
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,16 @@
+{
+    "build": { "dockerfile": "Dockerfile" },
+    "remoteUser": "user",
+    "runArgs": [],
+    "extensions": [
+		"ms-vscode.cpptools",
+		"twxs.cmake",
+		"ms-vscode.cmake-tools",
+		"ms-azuretools.vscode-docker",
+		"jbenden.c-cpp-flylint",
+		"eamodio.gitlens",
+		"Gruntfuggly.todo-tree",
+		"ms-vscode.cpptools-extension-pack",
+		"mine.cpplint"
+	]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index b5b85f28e1b01a6a6f20ad2c913e647d3d8095df..c6f7c1de41fad69133ad3483300676bc5fef44d4 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -67,5 +67,5 @@
         "variant": "cpp",
         "any": "cpp"
     },
-    "cmake.cmakePath": "/snap/cmake/current/bin/cmake"
+    "cmake.cmakePath": "cmake"
 }
\ No newline at end of file
diff --git a/examples/create-network-stream/main.cpp b/examples/create-network-stream/main.cpp
index 2608904e19d50f4d8dbf6e1e62776890d22074e7..914d09be65517d2128c7652c46f0db54a7e13d35 100644
--- a/examples/create-network-stream/main.cpp
+++ b/examples/create-network-stream/main.cpp
@@ -8,10 +8,13 @@
 #include <ftl/protocol.hpp>
 #include <ftl/protocol/streams.hpp>
 #include <ftl/protocol/self.hpp>
+#include <ftl/protocol/node.hpp>
 #include <ftl/time.hpp>
 #include <ftl/protocol/channels.hpp>
 #include <ftl/protocol/codecs.hpp>
 #include <ftl/lib/loguru.hpp>
+#include <ftl/protocol/muxer.hpp>
+#include <nlohmann/json.hpp>
 
 using ftl::protocol::StreamPacket;
 using ftl::protocol::DataPacket;
@@ -21,19 +24,43 @@ using ftl::protocol::Channel;
 using ftl::protocol::Codec;
 
 int main(int argc, char *argv[]) {
-    if (argc != 2) return -1;
+    if (argc != 3) return -1;
+
+    ftl::getSelf()->onNodeDetails([]() {
+        return nlohmann::json{
+            {"id", ftl::protocol::id.to_string()},
+            {"title", "Test app"},
+            { "gpus", nlohmann::json::array() },
+            { "devices", nlohmann::json::array() }
+        };
+    });
 
     ftl::getSelf()->listen("tcp://localhost:9000");
+    auto node = ftl::connectNode(argv[1]);
+    node->waitConnection();
+
+    auto muxer = std::make_unique<ftl::protocol::Muxer>();
+    muxer->begin();
 
-    auto stream = ftl::createStream(argv[1]);
+    auto stream = ftl::createStream(argv[2]);
+    muxer->add(stream);
 
-    auto h = stream->onError([](ftl::protocol::Error, const std::string &str) {
+    auto h = muxer->onError([](ftl::protocol::Error, const std::string &str) {
         LOG(ERROR) << str;
         return true;
     });
 
+    auto rh = muxer->onRequest([](const ftl::protocol::Request &r) {
+        LOG(INFO) << "Got request " << r.id.frameset() << "," << r.id.source();
+        return true;
+    });
+
+    stream->seen(ftl::protocol::FrameID(0, 0), Channel::kEndFrame);
+
     if (!stream->begin()) return -1;
 
+    sleep_for(milliseconds(100));
+
     int count = 10;
     while (count--) {
         StreamPacket spkt;
@@ -49,7 +76,9 @@ int main(int argc, char *argv[]) {
         sleep_for(milliseconds(100));
     }
 
-    stream->end();
+    LOG(INFO) << "Done";
+
+    muxer->end();
 
     return 0;
 }
diff --git a/src/streams/muxer.cpp b/src/streams/muxer.cpp
index a5b425e235b9b5df4ba59634f456e19a993abbbd..9c926b702062b9e59d8299b3be03d74728be6549 100644
--- a/src/streams/muxer.cpp
+++ b/src/streams/muxer.cpp
@@ -36,22 +36,26 @@ FrameID Muxer::_mapFromInput(Muxer::StreamEntry *s, FrameID id) {
         // Otherwise allocate something.
         lk.unlock();
         UNIQUE_LOCK(mutex_, ulk);
-
-        FrameID newID;
-        if (s->fixed_fs >= 0) {
-            int source = sourcecount_[s->fixed_fs]++;
-            newID = FrameID(s->fixed_fs, source);
+        auto it2 = imap_.find(iid);
+        if (it2 != imap_.end()) {
+            return it2->second;
         } else {
-            int fsiid = (s->id << 16) | id.frameset();
-            if (fsmap_.count(fsiid) == 0) fsmap_[fsiid] = framesets_++;
-            newID = FrameID(fsmap_[fsiid], id.source());
-        }
+            FrameID newID;
+            if (s->fixed_fs >= 0) {
+                int source = sourcecount_[s->fixed_fs]++;
+                newID = FrameID(s->fixed_fs, source);
+            } else {
+                int fsiid = (s->id << 16) | id.frameset();
+                if (fsmap_.count(fsiid) == 0) fsmap_[fsiid] = framesets_++;
+                newID = FrameID(fsmap_[fsiid], id.source());
+            }
 
-        imap_[iid] = newID;
-        auto &op = omap_[newID];
-        op.first = id;
-        op.second = s;
-        return newID;
+            imap_[iid] = newID;
+            auto &op = omap_[newID];
+            op.first = id;
+            op.second = s;
+            return newID;
+        }
     }
 }