incremental
diff --git a/include/ast_video_puller.hpp b/include/ast_video_puller.hpp
index 214e2df..fd29ca5 100644
--- a/include/ast_video_puller.hpp
+++ b/include/ast_video_puller.hpp
@@ -2,73 +2,76 @@
 
 #include <assert.h>
 #include <ast_video_types.hpp>
+#include <g3log/g3log.hpp>
 #include <iostream>
+#include <mutex>
 #include <vector>
+#include <boost/asio.hpp>
 
 namespace AstVideo {
-class VideoPuller {
-  //
-  // Cursor struct is used in User Mode
-  //
-  typedef struct _cursor_attribution_tag {
-    unsigned int posX;
-    unsigned int posY;
-    unsigned int cur_width;
-    unsigned int cur_height;
-    unsigned int cur_type;  // 0:mono 1:color 2:disappear cursor
-    unsigned int cur_change_flag;
-  } AST_CUR_ATTRIBUTION_TAG;
 
-  //
-  // For storing Cursor Information
-  //
-  typedef struct _cursor_tag {
-    AST_CUR_ATTRIBUTION_TAG attr;
-    // unsigned char     icon[MAX_CUR_OFFSETX*MAX_CUR_OFFSETY*2];
-    unsigned char *icon;  //[64*64*2];
-  } AST_CURSOR_TAG;
+//
+// Cursor struct is used in User Mode
+//
+typedef struct _cursor_attribution_tag {
+  unsigned int posX;
+  unsigned int posY;
+  unsigned int cur_width;
+  unsigned int cur_height;
+  unsigned int cur_type;  // 0:mono 1:color 2:disappear cursor
+  unsigned int cur_change_flag;
+} AST_CUR_ATTRIBUTION_TAG;
 
-  //
-  // For select image format, i.e. 422 JPG420, 444 JPG444, lumin/chrom table, 0
-  // ~ 11, low to high
-  //
-  typedef struct _video_features {
-    short jpg_fmt;  // 422:JPG420, 444:JPG444
-    short lumin_tbl;
-    short chrom_tbl;
-    short tolerance_noise;
-    int w;
-    int h;
-    unsigned char *buf;
-  } FEATURES_TAG;
+//
+// For storing Cursor Information
+//
+typedef struct _cursor_tag {
+  AST_CUR_ATTRIBUTION_TAG attr;
+  // unsigned char     icon[MAX_CUR_OFFSETX*MAX_CUR_OFFSETY*2];
+  unsigned char *icon;  //[64*64*2];
+} AST_CURSOR_TAG;
 
-  //
-  // For configure video engine control registers
-  //
-  typedef struct _image_info {
-    short do_image_refresh;  // Action 0:motion 1:fullframe 2:quick cursor
-    char qc_valid;           // quick cursor enable/disable
-    unsigned int len;
-    int crypttype;
-    char cryptkey[16];
-    union {
-      FEATURES_TAG features;
-      AST_CURSOR_TAG cursor_info;
-    } parameter;
-  } IMAGE_INFO;
+//
+// For select image format, i.e. 422 JPG420, 444 JPG444, lumin/chrom table, 0
+// ~ 11, low to high
+//
+typedef struct _video_features {
+  short jpg_fmt;  // 422:JPG420, 444:JPG444
+  short lumin_tbl;
+  short chrom_tbl;
+  short tolerance_noise;
+  int w;
+  int h;
+  unsigned char *buf;
+} FEATURES_TAG;
 
+//
+// For configure video engine control registers
+//
+typedef struct _image_info {
+  short do_image_refresh;  // Action 0:motion 1:fullframe 2:quick cursor
+  char qc_valid;           // quick cursor enable/disable
+  unsigned int len;
+  int crypttype;
+  char cryptkey[16];
+  union {
+    FEATURES_TAG features;
+    AST_CURSOR_TAG cursor_info;
+  } parameter;
+} IMAGE_INFO;
+
+class SimpleVideoPuller {
  public:
-  VideoPuller() : image_info(){};
+  SimpleVideoPuller() : image_info(){};
 
   void initialize() {
     std::cout << "Opening /dev/video\n";
     video_fd = open("/dev/video", O_RDWR);
     if (!video_fd) {
       std::cout << "Failed to open /dev/video\n";
-      // TODO(Ed) throw exception?
-    } else {
-      std::cout << "Opened successfully\n";
+      throw std::runtime_error("Failed to open /dev/video");
     }
+    std::cout << "Opened successfully\n";
   }
 
   RawVideoBuffer read_video() {
@@ -93,13 +96,13 @@
 
     std::cout << "Write done\n";
     */
-    std::cout << "Reading\n";
+    LOG(DEBUG) << "Reading\n";
     status = read(video_fd, reinterpret_cast<char *>(&image_info),
                   sizeof(image_info));
-    std::cout << "Reading\n";
+    LOG(DEBUG) << "Done reading\n";
 
     if (status != 0) {
-      std::cout << "Read failed with status " << status << "\n";
+      LOG(WARNING) << "Read failed with status " << status << "\n";
     }
 
     raw.buffer.resize(image_info.len);
@@ -118,4 +121,66 @@
   int video_fd;
   IMAGE_INFO image_info;
 };
+
+#if defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR)
+class AsyncVideoPuller {
+ public:
+  typedef std::function<void(RawVideoBuffer &)> video_callback;
+
+  AsyncVideoPuller(boost::asio::io_service &io_service)
+      : image_info(), dev_video(io_service, open("/dev/video", O_RDWR)) {
+    videobuf = std::make_shared<RawVideoBuffer>();
+
+    image_info.do_image_refresh = 1;  // full frame refresh
+    image_info.qc_valid = 0;          // quick cursor disabled
+    image_info.parameter.features.buf =
+        reinterpret_cast<unsigned char *>(videobuf->buffer.data());
+    image_info.crypttype = -1;
+  };
+
+  void register_callback(video_callback &callback) {
+    std::lock_guard<std::mutex> lock(callback_mutex);
+    callbacks.push_back(callback);
+    start_read();
+  }
+
+  void start_read() {
+    auto mutable_buffer = boost::asio::buffer(&image_info, sizeof(image_info));
+    boost::asio::async_read(
+        dev_video, mutable_buffer, [this](const boost::system::error_code &ec,
+                                          std::size_t bytes_transferred) {
+          if (ec) {
+            LOG(WARNING) << "Read failed with status " << ec << "\n";
+          } else {
+            this->read_done();
+          }
+        });
+  }
+
+  void read_done() {
+    LOG(DEBUG) << "Done reading\n";
+    videobuf->buffer.resize(image_info.len);
+
+    videobuf->height = image_info.parameter.features.h;
+    videobuf->width = image_info.parameter.features.w;
+    if (image_info.parameter.features.jpg_fmt == 422) {
+      videobuf->mode = YuvMode::YUV420;
+    } else {
+      videobuf->mode = YuvMode::YUV444;
+    }
+    std::lock_guard<std::mutex> lock(callback_mutex);
+    for (auto &callback : callbacks) {
+      // TODO(ed) call callbacks async and double buffer frames
+      callback(*videobuf);
+    }
+  }
+
+ private:
+  std::shared_ptr<RawVideoBuffer> videobuf;
+  boost::asio::posix::stream_descriptor dev_video;
+  IMAGE_INFO image_info;
+  std::mutex callback_mutex;
+  std::vector<video_callback> callbacks;
+};
+#endif  // defined(BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR)
 }
diff --git a/include/web_kvm.hpp b/include/web_kvm.hpp
index 10291ca..1671382 100644
--- a/include/web_kvm.hpp
+++ b/include/web_kvm.hpp
@@ -282,7 +282,7 @@
                     // Todo(ed) lifecycle of the video puller and decoder
                     // should be
                     // with the websocket, not recreated every time
-                    AstVideo::VideoPuller p;
+                    AstVideo::SimpleVideoPuller p;
                     p.initialize();
                     auto out = p.read_video();
                     AstVideo::AstJpegDecoder d;