diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..f860252ae752f4255b8d17b6df94e61777816bfa
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,122 @@
+
+# Created by https://www.toptal.com/developers/gitignore/api/qt,qtcreator,c++
+# Edit at https://www.toptal.com/developers/gitignore?templates=qt,qtcreator,c++
+
+### C++ ###
+# Prerequisites
+*.d
+
+# Compiled Object files
+*.slo
+*.lo
+*.o
+*.obj
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Linker files
+*.ilk
+
+# Debugger Files
+*.pdb
+
+# Compiled Dynamic libraries
+*.so
+*.dylib
+*.dll
+
+# Fortran module files
+*.mod
+*.smod
+
+# Compiled Static libraries
+*.lai
+*.la
+*.a
+*.lib
+
+# Executables
+*.exe
+*.out
+*.app
+
+### Qt ###
+# C++ objects and libs
+*.so.*
+
+# Qt-es
+object_script.*.Release
+object_script.*.Debug
+*_plugin_import.cpp
+/.qmake.cache
+/.qmake.stash
+*.pro.user
+*.pro.user.*
+*.qbs.user
+*.qbs.user.*
+*.moc
+moc_*.cpp
+moc_*.h
+qrc_*.cpp
+ui_*.h
+*.qmlc
+*.jsc
+Makefile*
+*build-*
+*.qm
+*.prl
+
+# Qt unit tests
+target_wrapper.*
+
+# QtCreator
+*.autosave
+
+# QtCreator Qml
+*.qmlproject.user
+*.qmlproject.user.*
+
+# QtCreator CMake
+CMakeLists.txt.user*
+
+# QtCreator 4.8< compilation database 
+compile_commands.json
+
+# QtCreator local machine specific files for imported projects
+*creator.user*
+
+### QtCreator ###
+# gitignore for Qt Creator like IDE for pure C/C++ project without Qt
+#
+# Reference: http://doc.qt.io/qtcreator/creator-project-generic.html
+
+
+
+# Qt Creator autogenerated files
+
+
+# A listing of all the files included in the project
+*.files
+
+# Include directories
+*.includes
+
+# Project configuration settings like predefined Macros
+*.config
+
+# Qt Creator settings
+*.creator
+
+# User project settings
+*.creator.user*
+
+# Qt Creator backups
+
+# Flags for Clang Code Model
+*.cxxflags
+*.cflags
+
+
+# End of https://www.toptal.com/developers/gitignore/api/qt,qtcreator,c++
\ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..a92b66b036f8fb457ae5eb6acd4b682cec9bc0df
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,24 @@
+cmake_minimum_required(VERSION 3.17)
+
+project(
+  libmueb
+  VERSION 4.0
+  DESCRIPTION "SchĂśnherz MĂĄtrix network library written in C++ using Qt"
+  LANGUAGES CXX)
+
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+
+set(CMAKE_AUTOUIC OFF)
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTORCC OFF)
+
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_CXX_EXTENSIONS OFF)
+
+find_package(
+  Qt6
+  COMPONENTS Core Gui Network Concurrent
+  REQUIRED)
+
+add_subdirectory(src)
diff --git a/include/libmueb/libmueb_global.h b/include/libmueb/libmueb_global.h
new file mode 100644
index 0000000000000000000000000000000000000000..51696cdc72594eb664ea963c8909c673b17e5f84
--- /dev/null
+++ b/include/libmueb/libmueb_global.h
@@ -0,0 +1,19 @@
+#ifndef LIBMUEB_LIBMUEB_GLOBAL_H_
+#define LIBMUEB_LIBMUEB_GLOBAL_H_
+
+#include <QtCore/qglobal.h>
+
+#include <QImage>
+
+namespace libmueb {
+// One frame to be transmitted/received. Just an alias for user convenience.
+using Frame = QImage;
+}  // namespace libmueb
+
+#if defined(LIBMUEB_LIBRARY)
+#define LIBMUEB_EXPORT Q_DECL_EXPORT
+#else
+#define LIBMUEB_EXPORT Q_DECL_IMPORT
+#endif
+
+#endif  // LIBMUEB_LIBMUEB_GLOBAL_H_
diff --git a/include/libmueb/muebreceiver.h b/include/libmueb/muebreceiver.h
new file mode 100644
index 0000000000000000000000000000000000000000..4c5c0403b0fbf1d30e5c29cf3af995732f5bddce
--- /dev/null
+++ b/include/libmueb/muebreceiver.h
@@ -0,0 +1,31 @@
+#ifndef LIBMUEB_MUEBRECEIVER_H_
+#define LIBMUEB_MUEBRECEIVER_H_
+
+#include <QObject>
+
+#include "libmueb_global.h"
+
+class MuebReceiverPrivate;
+
+class LIBMUEB_EXPORT MuebReceiver final : public QObject {
+  Q_OBJECT
+  Q_DECLARE_PRIVATE_D(d_ptr_, MuebReceiver)
+  Q_DISABLE_COPY(MuebReceiver)
+
+ public:
+  static MuebReceiver& Instance();
+  libmueb::Frame frame();
+
+ signals:
+  void FrameChanged(libmueb::Frame frame);
+
+ private:
+  MuebReceiverPrivate* d_ptr_;
+
+  explicit MuebReceiver(QObject* parent = nullptr);
+  ~MuebReceiver();
+
+  void readPendingDatagrams();
+};
+
+#endif  // LIBMUEB_MUEBRECEIVER_H_
diff --git a/include/libmueb/muebtransmitter.h b/include/libmueb/muebtransmitter.h
new file mode 100644
index 0000000000000000000000000000000000000000..0316a3bbfb4c70a80bb54b2ba7345edc539b716f
--- /dev/null
+++ b/include/libmueb/muebtransmitter.h
@@ -0,0 +1,34 @@
+#ifndef LIBMUEB_MUEBTRANSMITTER_H_
+#define LIBMUEB_MUEBTRANSMITTER_H_
+
+#include <QObject>
+#include <cstdint>
+
+#include "libmueb_global.h"
+
+class MuebTransmitterPrivate;
+
+class LIBMUEB_EXPORT MuebTransmitter final : public QObject {
+  Q_OBJECT
+  Q_DECLARE_PRIVATE_D(d_ptr_, MuebTransmitter)
+  Q_DISABLE_COPY(MuebTransmitter)
+
+ public:
+  void SendFrame(libmueb::Frame frame);
+
+  static MuebTransmitter& Instance();
+
+  std::int32_t width() const;
+
+  std::int32_t height() const;
+
+  libmueb::Frame frame() const;
+
+ private:
+  MuebTransmitterPrivate* d_ptr_;
+
+  explicit MuebTransmitter(QObject* parent = nullptr);
+  ~MuebTransmitter();
+};
+
+#endif  // LIBMUEB_MUEBTRANSMITTER_H_
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d698c4297cc83a1d87eff78d2f993baa09439969
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,23 @@
+add_library(
+  muebtransmitter SHARED
+  ${CMAKE_SOURCE_DIR}/include/libmueb/libmueb_global.h
+  ${CMAKE_SOURCE_DIR}/include/libmueb/muebtransmitter.h configuration.h
+  muebtransmitter.cc configuration.cc)
+target_include_directories(muebtransmitter PUBLIC ../include/${PROJECT_NAME})
+target_link_libraries(
+  muebtransmitter
+  PUBLIC Qt6::Core Qt6::Gui
+  PRIVATE Qt6::Network Qt6::Concurrent)
+target_compile_definitions(muebtransmitter PRIVATE LIBMUEB_LIBRARY)
+
+add_library(
+  muebreceiver SHARED
+  ${CMAKE_SOURCE_DIR}/include/libmueb/libmueb_global.h
+  ${CMAKE_SOURCE_DIR}/include/libmueb/muebreceiver.h configuration.h
+  muebreceiver.cc configuration.cc)
+target_include_directories(muebreceiver PUBLIC ../include/${PROJECT_NAME})
+target_link_libraries(
+  muebreceiver
+  PUBLIC Qt6::Core Qt6::Gui
+  PRIVATE Qt6::Network)
+target_compile_definitions(muebreceiver PRIVATE LIBMUEB_LIBRARY)
diff --git a/src/configuration.cc b/src/configuration.cc
new file mode 100644
index 0000000000000000000000000000000000000000..710b17566115ae64ccf98374d8726c2c2eee6b53
--- /dev/null
+++ b/src/configuration.cc
@@ -0,0 +1,147 @@
+#include "configuration.h"
+
+#include <cmath>
+
+Configuration::Configuration(QObject *parent)
+    : QObject(parent), settings_("matrix-group", "libmueb") {
+  LoadSettings();
+}
+
+QImage Configuration::frame() const { return frame_; }
+
+QHostAddress Configuration::target_address() const { return target_address_; }
+
+std::uint32_t Configuration::floors() const { return floors_; }
+
+std::uint32_t Configuration::rooms_per_floor() const {
+  return rooms_per_floor_;
+}
+
+std::uint32_t Configuration::windows_per_room() const {
+  return windows_per_room_;
+}
+
+std::uint32_t Configuration::vertical_pixel_unit() const {
+  return vertical_pixel_unit_;
+}
+
+std::uint32_t Configuration::horizontal_pixel_unit() const {
+  return horizontal_pixel_unit_;
+}
+
+std::uint32_t Configuration::pixels_per_window() const {
+  return pixels_per_window_;
+}
+
+std::uint32_t Configuration::window_per_floor() const {
+  return window_per_floor_;
+}
+
+std::uint32_t Configuration::windows() const { return windows_; }
+
+std::uint32_t Configuration::pixels() const { return pixels_; }
+
+std::int32_t Configuration::width() const { return width_; }
+
+std::int32_t Configuration::height() const { return height_; }
+
+std::uint8_t Configuration::protocol_type() const { return protocol_type_; }
+
+std::uint32_t Configuration::window_byte_size() const {
+  return window_byte_size_;
+}
+
+std::uint32_t Configuration::max_windows_per_datagram() const {
+  return max_windows_per_datagram_;
+}
+
+std::uint32_t Configuration::packet_header_size() const {
+  return packet_header_size_;
+}
+
+std::uint32_t Configuration::packet_size() const { return packet_size_; }
+
+std::uint32_t Configuration::packet_payload_size() const {
+  return packet_payload_size_;
+}
+
+std::uint32_t Configuration::max_pixel_per_datagram() const {
+  return max_pixel_per_datagram_;
+}
+
+std::uint32_t Configuration::remainder_packet_size() const {
+  return remainder_packet_size_;
+}
+
+std::uint16_t Configuration::unicast_animation_port() const {
+  return unicast_animation_port_;
+}
+
+std::uint16_t Configuration::broadcast_animation_port() const {
+  return broadcast_animation_port_;
+}
+
+std::uint8_t Configuration::max_packet_number() const {
+  return max_packet_number_;
+}
+
+std::uint8_t Configuration::color_depth() const { return color_depth_; }
+
+std::uint8_t Configuration::factor() const { return factor_; }
+
+bool Configuration::debug_mode() const { return debug_mode_; }
+
+void Configuration::LoadSettings() {
+  // Building specific constants
+  floors_ = settings_.value("floors", 13).toUInt();
+  rooms_per_floor_ = settings_.value("rooms_per_floor", 8).toUInt();
+  windows_per_room_ = settings_.value("windows_per_room", 2).toUInt();
+
+  // Hardware specific constants
+  vertical_pixel_unit_ = settings_.value("vertical_pixel_unit", 2).toUInt();
+  horizontal_pixel_unit_ = settings_.value("horizontal_pixel_unit", 2).toUInt();
+  color_depth_ = settings_.value("color_depth", 3).toUInt();
+
+  // Software specific constants
+  pixels_per_window_ = vertical_pixel_unit_ * horizontal_pixel_unit_;
+  window_per_floor_ = rooms_per_floor_ * windows_per_room_;
+  windows_ = floors_ * window_per_floor_;
+  pixels_ = windows_ * pixels_per_window_;
+  width_ = window_per_floor_ * horizontal_pixel_unit_;
+  height_ = floors_ * vertical_pixel_unit_;
+  factor_ = 8 - color_depth_;
+
+  frame_ = QImage(width_, height_, QImage::Format_RGB888);
+  frame_.fill(Qt::black);
+
+  // Network protocol specific constants
+  protocol_type_ = 2;
+  unicast_animation_port_ =
+      settings_.value("unicast_animation_port", 3000).toUInt();
+  broadcast_animation_port_ =
+      settings_.value("broadcast_animation_port", 10000).toUInt();
+
+  // Debug specific constants
+  // Send packets to localhost
+  debug_mode_ = settings_.value("debugMode", false).toBool();
+
+  target_address_ =
+      (debug_mode_)
+          ? QHostAddress("127.0.0.1")
+          : QHostAddress(
+                settings_.value("target_address", "10.6.255.255").toString());
+  window_byte_size_ = (color_depth_ >= 3 && color_depth_ < 5)
+                          ? pixels_per_window_ * 3 / 2
+                          : pixels_per_window_ * 3;
+  max_windows_per_datagram_ =
+      settings_.value("max_windows_per_datagram", windows_).toUInt();
+  packet_header_size_ = 2;
+  packet_size_ =
+      packet_header_size_ + max_windows_per_datagram_ * window_byte_size_;
+  packet_payload_size_ = max_windows_per_datagram_ * window_byte_size_;
+  max_pixel_per_datagram_ = max_windows_per_datagram_ * pixels_per_window_;
+  max_packet_number_ = static_cast<std::uint32_t>(
+      std::ceil(static_cast<float>(windows_) / max_windows_per_datagram_));
+
+  // TODO Configuration check
+}
diff --git a/src/configuration.h b/src/configuration.h
new file mode 100644
index 0000000000000000000000000000000000000000..5156f2c19d2bd89eed15058b7136cf001895df5a
--- /dev/null
+++ b/src/configuration.h
@@ -0,0 +1,107 @@
+#ifndef LIBMUEB_CONFIGURATION_H_
+#define LIBMUEB_CONFIGURATION_H_
+
+#include <QHostAddress>
+#include <QImage>
+#include <QObject>
+#include <QSettings>
+#include <cstdint>
+
+// TODO check, remove unused parameters
+// TODO check, variable types
+class Configuration : public QObject {
+  Q_OBJECT
+
+ public:
+  explicit Configuration(QObject *parent = nullptr);
+
+  QImage frame() const;
+
+  QHostAddress target_address() const;
+
+  std::uint32_t floors() const;
+
+  std::uint32_t rooms_per_floor() const;
+
+  std::uint32_t windows_per_room() const;
+
+  std::uint32_t vertical_pixel_unit() const;
+
+  std::uint32_t horizontal_pixel_unit() const;
+
+  std::uint32_t pixels_per_window() const;
+
+  std::uint32_t window_per_floor() const;
+
+  std::uint32_t windows() const;
+
+  std::uint32_t pixels() const;
+
+  std::int32_t width() const;
+
+  std::int32_t height() const;
+
+  std::uint8_t protocol_type() const;
+
+  std::uint32_t window_byte_size() const;
+
+  std::uint32_t max_windows_per_datagram() const;
+
+  std::uint32_t packet_header_size() const;
+
+  std::uint32_t packet_size() const;
+
+  std::uint32_t packet_payload_size() const;
+
+  std::uint32_t max_pixel_per_datagram() const;
+
+  std::uint32_t remainder_packet_size() const;
+
+  std::uint16_t unicast_animation_port() const;
+
+  std::uint16_t broadcast_animation_port() const;
+
+  std::uint8_t max_packet_number() const;
+
+  std::uint8_t color_depth() const;
+
+  std::uint8_t factor() const;
+
+  bool debug_mode() const;
+
+ private:
+  QImage frame_;
+  QSettings settings_;
+  QHostAddress target_address_;
+  std::uint32_t floors_;
+  std::uint32_t rooms_per_floor_;
+  std::uint32_t windows_per_room_;
+  std::uint32_t vertical_pixel_unit_;
+  std::uint32_t horizontal_pixel_unit_;
+  std::uint32_t pixels_per_window_;
+  std::uint32_t window_per_floor_;
+  std::uint32_t windows_;
+  std::uint32_t pixels_;
+  // Qt width, height is signed
+  std::int32_t width_;
+  std::int32_t height_;
+  //
+  std::uint8_t protocol_type_;
+  std::uint32_t window_byte_size_;
+  std::uint32_t max_windows_per_datagram_;
+  std::uint32_t packet_header_size_;
+  std::uint32_t packet_size_;
+  std::uint32_t packet_payload_size_;
+  std::uint32_t max_pixel_per_datagram_;
+  std::uint32_t remainder_packet_size_;
+  std::uint16_t unicast_animation_port_;
+  std::uint16_t broadcast_animation_port_;
+  std::uint8_t max_packet_number_;
+  std::uint8_t color_depth_;
+  std::uint8_t factor_;
+  bool debug_mode_;
+
+  void LoadSettings();
+};
+
+#endif  // LIBMUEB_CONFIGURATION_H_
diff --git a/src/muebreceiver.cc b/src/muebreceiver.cc
new file mode 100644
index 0000000000000000000000000000000000000000..dade9e84f88eaee8d1d97a7368a7ae3028978570
--- /dev/null
+++ b/src/muebreceiver.cc
@@ -0,0 +1,102 @@
+#include "muebreceiver.h"
+
+#include <QNetworkDatagram>
+#include <QUdpSocket>
+
+#include "configuration.h"
+
+class MuebReceiverPrivate {
+  Q_DECLARE_PUBLIC(MuebReceiver)
+  Q_DISABLE_COPY(MuebReceiverPrivate)
+
+ public:
+  explicit MuebReceiverPrivate(MuebReceiver *q)
+      : frame(configuration.frame()), q_ptr(q) {
+    socket.bind(configuration.broadcast_animation_port());
+
+    QObject::connect(&socket, &QUdpSocket::readyRead, q,
+                     &MuebReceiver::readPendingDatagrams);
+
+    qInfo() << "[MuebReceiver] UDP Socket will receive packets on port"
+            << configuration.broadcast_animation_port();
+  }
+
+  Configuration configuration;
+  libmueb::Frame frame;
+  QUdpSocket socket;
+  MuebReceiver *q_ptr;
+};
+
+MuebReceiver::MuebReceiver(QObject *parent)
+    : QObject(parent), d_ptr_(new MuebReceiverPrivate(this)) {}
+
+MuebReceiver::~MuebReceiver() { delete d_ptr_; }
+
+MuebReceiver &MuebReceiver::Instance() {
+  static MuebReceiver instance;
+
+  return instance;
+}
+
+libmueb::Frame MuebReceiver::frame() { return d_ptr_->frame; }
+
+inline void datagram_uncompress_error() {
+  qWarning() << "[MuebReceiver] Processed packet is invalid! Check the header "
+                "or packet contents(size)";
+}
+
+void MuebReceiver::readPendingDatagrams() {
+  while (d_ptr_->socket.hasPendingDatagrams()) {
+    if (d_ptr_->socket.pendingDatagramSize() ==
+        d_ptr_->configuration.packet_size()) {
+      QNetworkDatagram datagram = d_ptr_->socket.receiveDatagram();
+      QByteArray data = datagram.data();
+
+      // Process datagram
+      // Packet header check
+      // Check protocol
+      if (data[0] != d_ptr_->configuration.protocol_type()) {
+        datagram_uncompress_error();
+        return;
+      }
+
+      auto packet_number = data[1];
+      if (packet_number >= d_ptr_->configuration.max_packet_number() ||
+          packet_number < 0) {
+        datagram_uncompress_error();
+        return;
+      }
+
+      data.remove(0, d_ptr_->configuration.packet_header_size());
+      auto frame_begin =
+          d_ptr_->frame.bits() +
+          packet_number * d_ptr_->configuration.max_pixel_per_datagram();
+
+      // Uncompress 1 byte into 2 color components
+      if (d_ptr_->configuration.color_depth() < 5) {
+        for (auto i : data) {
+          *frame_begin = i & 0xf0;
+          frame_begin++;
+          *frame_begin = (i & 0x0f) << d_ptr_->configuration.factor();
+          frame_begin++;
+        }
+        // No compression
+      } else {
+        // FIXME use better copy method
+        for (auto i : data) {
+          *frame_begin = i;
+          frame_begin++;
+        }
+      }
+
+      emit(FrameChanged(d_ptr_->frame));
+    }
+    // Drop invalid packet
+    else {
+      qWarning() << "[MuebReceiver] Packet has invalid size!"
+                 << d_ptr_->socket.pendingDatagramSize() << "bytes";
+
+      d_ptr_->socket.receiveDatagram(0);
+    }
+  }
+}
diff --git a/src/muebtransmitter.cc b/src/muebtransmitter.cc
new file mode 100644
index 0000000000000000000000000000000000000000..7f36005108f3f06d90b5f0fbd8831282b8801b6d
--- /dev/null
+++ b/src/muebtransmitter.cc
@@ -0,0 +1,128 @@
+#include "muebtransmitter.h"
+
+#include <QByteArray>
+#include <QDebug>
+#include <QNetworkDatagram>
+#include <QUdpSocket>
+#include <QtConcurrent>
+
+#include "configuration.h"
+
+class MuebTransmitterPrivate {
+  Q_DECLARE_PUBLIC(MuebTransmitter)
+  Q_DISABLE_COPY(MuebTransmitterPrivate)
+
+ public:
+  explicit MuebTransmitterPrivate(MuebTransmitter* q)
+      : datagram_(QByteArray(), configuration_.target_address(),
+                  configuration_.broadcast_animation_port()),
+        q_ptr(q) {
+    qInfo().noquote() << "[MuebTransmitter] UDP Socket will send frame to"
+                      << QString("%1:%2")
+                             .arg(configuration_.target_address().toString())
+                             .arg(configuration_.broadcast_animation_port());
+  }
+
+  void SendFrame(libmueb::Frame frame) {
+    std::uint8_t packet_number{0};
+
+    QByteArray reduced_compressed_frame;
+    // Frame color reduction and compression
+    if (configuration_.color_depth() < 5) {
+      reduced_compressed_frame =
+          QtConcurrent::blockingMappedReduced<QByteArray>(
+              frame.constBits(), frame.constBits() + frame.sizeInBytes(),
+              /* Reference:
+               * http://threadlocalmutex.com/?p=48
+               * http://threadlocalmutex.com/?page_id=60
+               */
+              [this](const uchar& color) -> uchar {
+                if (configuration_.color_depth() == 3) {
+                  return (color * 225 + 4096) >> 13;
+                } else if (configuration_.color_depth() == 4) {
+                  return (color * 15 + 135) >> 8;
+                }
+
+                return color;
+              },
+              [this](QByteArray& compressed_colors, const uchar& color) {
+                static bool msb{true};
+
+                // Compress 2 color components into 1 byte
+                if (msb) {
+                  compressed_colors.append(color << configuration_.factor());
+                } else {
+                  compressed_colors.back() = compressed_colors.back() | color;
+                }
+
+                msb = !msb;
+              },
+              QtConcurrent::OrderedReduce | QtConcurrent::SequentialReduce);
+    }
+    // No compression
+    else {
+      reduced_compressed_frame.setRawData(
+          reinterpret_cast<const char*>(frame.bits()), frame.sizeInBytes());
+    }
+
+    for (std::uint8_t i = 0; i < configuration_.max_packet_number(); ++i) {
+      if (configuration_.max_packet_number() == 1) {
+        reduced_compressed_frame.insert(0, configuration_.protocol_type())
+            .insert(1, packet_number);
+
+        datagram_.setData(reduced_compressed_frame);
+      } else {
+        QByteArray data;
+        data.append(configuration_.protocol_type())
+            .append(packet_number++)
+            .append(reduced_compressed_frame.sliced(
+                i * configuration_.packet_payload_size(),
+                configuration_.packet_payload_size()));
+
+        datagram_.setData(data);
+      }
+
+      socket_.writeDatagram(datagram_);
+    }
+  }
+
+  Configuration configuration_;
+  QUdpSocket socket_;
+  QNetworkDatagram datagram_;
+  MuebTransmitter* q_ptr;
+};
+
+MuebTransmitter::MuebTransmitter(QObject* parent)
+    : QObject(parent), d_ptr_(new MuebTransmitterPrivate(this)) {}
+
+MuebTransmitter::~MuebTransmitter() { delete d_ptr_; }
+
+void MuebTransmitter::SendFrame(libmueb::Frame frame) {
+  if (frame.isNull() || frame.format() == QImage::Format_Invalid ||
+      frame.width() != d_ptr_->configuration_.width() ||
+      frame.height() != d_ptr_->configuration_.height()) {
+    qWarning() << "[MuebTransmitter] Frame is invalid";
+    return;
+  }
+
+  frame.convertTo(QImage::Format_RGB888);
+  d_ptr_->SendFrame(frame);
+}
+
+MuebTransmitter& MuebTransmitter::Instance() {
+  static MuebTransmitter instance;
+
+  return instance;
+}
+
+int32_t MuebTransmitter::width() const {
+  return d_ptr_->configuration_.width();
+}
+
+int32_t MuebTransmitter::height() const {
+  return d_ptr_->configuration_.height();
+}
+
+libmueb::Frame MuebTransmitter::frame() const {
+  return d_ptr_->configuration_.frame();
+}