diff --git a/python/.gitignore b/python/.gitignore
index 7e99e367f8443d86e5e8825b9fda39dfbb39630d..feae5c1921703133caa7d3cb82a79e2be6e85ab6 100644
--- a/python/.gitignore
+++ b/python/.gitignore
@@ -1 +1,2 @@
-*.pyc
\ No newline at end of file
+*.pyc
+__pycache__
\ No newline at end of file
diff --git a/python/ftl/__init__.py b/python/ftl/__init__.py
index 136f6fe7d7396d7672817e5120c02d8096211310..e45ef9c2b544bde13d59925041f765c83090c0d0 100644
--- a/python/ftl/__init__.py
+++ b/python/ftl/__init__.py
@@ -1 +1,4 @@
-from . ftlstream import FTLStream
+from . ftlstream import FTLStreamReader, FTLStreamWriter
+from . misc import disparity_to_depth
+
+from . import ftltypes as types
\ No newline at end of file
diff --git a/python/ftl/ftlstream.py b/python/ftl/ftlstream.py
index ef8a32aba4a195f5d26fa7fa84cfecfeefda21b8..e12f197caf1b246d23a354b1aadcdcf58f6bdaf9 100644
--- a/python/ftl/ftlstream.py
+++ b/python/ftl/ftlstream.py
@@ -6,6 +6,9 @@ import struct
 
 from enum import IntEnum
 from collections import namedtuple
+
+from . misc import is_iframe
+from . import ftltypes as ftl
 from . import libde265
 
 try:
@@ -35,132 +38,43 @@ except ImportError:
 
         return rgb.round().astype(np.uint8)
 
-# FTL definitions
-
-# components/rgbd-sources/include/ftl/rgbd/camera.hpp
-_Camera = namedtuple("Camera", ["fx", "fy", "cx", "cy", "width", "height",
-                                "min_depth", "max_depth", "baseline", "doffs"])
-
-# components/codecs/include/ftl/codecs/packet.hpp
-_packet = namedtuple("Packet", ["codec", "definition", "block_total",
-                                "block_number", "flags", "data"])
-
-_stream_packet = namedtuple("StreamPacket", ["timestamp", "streamID",
-                                             "chanel_count", "channel"])
-
-# components/codecs/include/ftl/codecs/bitrates.hpp
-class _codec_t(IntEnum):
-    JPG = 0
-    PNG = 1
-    H264 = 2
-    HEVC = 3
-    WAV = 4
-    JSON = 100
-    CALIBRATION = 101
-    POSE = 102
-    RAW = 103
-
-_definition_t = {
-    0 : (7680, 4320),
-    1 : (2160, 3840),
-    2 : (1080, 1920),
-    3 : (720, 1280),
-    4 : (576, 1024),
-    5 : (480, 854),
-    6 : (360, 640),
-    7 : (0, 0),
-    8 : (2056, 1852)
-}
-
-# components/codecs/include/ftl/codecs/hevc.hpp
-class _NALType(IntEnum):
-    CODED_SLICE_TRAIL_N = 0
-    CODED_SLICE_TRAIL_R = 1
-
-    CODED_SLICE_TSA_N = 2
-    CODED_SLICE_TSA_R = 3
-
-    CODED_SLICE_STSA_N = 4
-    CODED_SLICE_STSA_R = 5
-
-    CODED_SLICE_RADL_N = 6
-    CODED_SLICE_RADL_R = 7
-
-    CODED_SLICE_RASL_N = 8
-    CODED_SLICE_RASL_R = 9
-
-    RESERVED_VCL_N10 = 10
-    RESERVED_VCL_R11 = 11
-    RESERVED_VCL_N12 = 12
-    RESERVED_VCL_R13 = 13
-    RESERVED_VCL_N14 = 14
-    RESERVED_VCL_R15 = 15
-
-    CODED_SLICE_BLA_W_LP = 16
-    CODED_SLICE_BLA_W_RADL = 17
-    CODED_SLICE_BLA_N_LP = 18
-    CODED_SLICE_IDR_W_RADL = 19
-    CODED_SLICE_IDR_N_LP = 20
-    CODED_SLICE_CRA = 21
-    RESERVED_IRAP_VCL22 = 22
-    RESERVED_IRAP_VCL23 = 23
-
-    RESERVED_VCL24 = 24
-    RESERVED_VCL25 = 25
-    RESERVED_VCL26 = 26
-    RESERVED_VCL27 = 27
-    RESERVED_VCL28 = 28
-    RESERVED_VCL29 = 29
-    RESERVED_VCL30 = 30
-    RESERVED_VCL31 = 31
-
-    VPS = 32
-    SPS = 33
-    PPS = 34
-    ACCESS_UNIT_DELIMITER = 35
-    EOS = 36
-    EOB = 37
-    FILLER_DATA = 38
-    PREFIX_SEI = 39
-    SUFFIX_SEI = 40
-
-    RESERVED_NVCL41 = 41
-    RESERVED_NVCL42 = 42
-    RESERVED_NVCL43 = 43
-    RESERVED_NVCL44 = 44
-    RESERVED_NVCL45 = 45
-    RESERVED_NVCL46 = 46
-    RESERVED_NVCL47 = 47
-    UNSPECIFIED_48 = 48
-    UNSPECIFIED_49 = 49
-    UNSPECIFIED_50 = 50
-    UNSPECIFIED_51 = 51
-    UNSPECIFIED_52 = 52
-    UNSPECIFIED_53 = 53
-    UNSPECIFIED_54 = 54
-    UNSPECIFIED_55 = 55
-    UNSPECIFIED_56 = 56
-    UNSPECIFIED_57 = 57
-    UNSPECIFIED_58 = 58
-    UNSPECIFIED_59 = 59
-    UNSPECIFIED_60 = 60
-    UNSPECIFIED_61 = 61
-    UNSPECIFIED_62 = 62
-    UNSPECIFIED_63 = 63
-    INVALID = 64
-
-def get_NAL_type(data):
-    if not isinstance(data, bytes):
-        raise ValueError("expected bytes")
-    
-    return _NALType((data[4] >> 1) & 0x3f)
+class FTLStreamWriter:
+    def __init__(self, file):
+        self._file = open(file, "wb")
+        self._file.write(bytes(ord(c) for c in "FTLF"))
+        self._file.write(bytes([0]))
+
+        self._packer = msgpack.Packer(strict_types=False, use_bin_type=True)
+
+    def __del__(self):
+        self.close()
+
+    def close(self):
+        self._file.close()
+
+    def add_source(self, parameters, pose):
+        pass
+
+    def add_raw(self, sp, p):
+        if len(sp) != len(ftl.StreamPacket._fields) or len(p) != len(ftl.Packet._fields):
+           raise ValueError("invalid input")
+        
+        self._file.write(self._packer.pack((sp, p)))
+
+    def add_frame(self, timestamp, src, channel, codec, data, encode=True):
+        pass
 
-class FTLStream:
+    def add_depth(self, timestamp, src, data):
+        pass
+
+class FTLStreamReader:
     ''' FTL file reader '''
     
     def __init__(self, file):
         self._file = open(file, "br")
         self._decoders = {}
+        self._seen_iframe = set()
+
         self._frames = {}
         self._calibration = {}
         self._pose = {}
@@ -168,8 +82,9 @@ class FTLStream:
 
         try:
             magic = self._file.read(5)
-            if magic[:4] != bytearray(ord(c) for c in "FTLF"):
+            if magic[:4] != bytes(ord(c) for c in "FTLF"):
                 raise Exception("wrong magic")
+            print(magic[4])
             
             self._unpacker = msgpack.Unpacker(self._file, raw=True, use_list=False)
             
@@ -184,7 +99,7 @@ class FTLStream:
     
     def _read_next(self):
         v1, v2 = self._unpacker.unpack()
-        return _stream_packet._make(v1), _packet._make(v2)
+        return ftl.StreamPacket._make(v1), ftl.Packet._make(v2)
     
     def _update_calib(self, sp, p):
         ''' Update calibration.
@@ -192,7 +107,7 @@ class FTLStream:
         todo: fix endianess
         '''
         calibration = struct.unpack("@ddddIIdddd", p.data[:(4*8+2*4+4*8)])
-        self._calibration[sp.streamID] = _Camera._make(calibration)
+        self._calibration[sp.streamID] = ftl.Camera._make(calibration)
 
     def _update_pose(self, sp, p):
         ''' Update pose
@@ -209,33 +124,34 @@ class FTLStream:
     def _process_json(self, sp, p):
         raise NotImplementedError("json decoding not implemented")
 
-    def _push_data_hevc(self, sp, p):
+    def _decode_hevc(self, sp, p):
         ''' Decode HEVC frame '''
         
         k = (sp.streamID, sp.channel)
         
         if k not in self._decoders:
-            self._decoders[k] = libde265.Decoder(_definition_t[p.definition])
+            self._decoders[k] = libde265.Decoder(ftl.definition_t[p.definition])
         
         decoder = self._decoders[k]
+
+        if k not in self._seen_iframe:
+            if not is_iframe(p.data):
+                # can't decode before first I-frame has been received
+                return
+            
+            self._seen_iframe.add(k)
+        
         decoder.push_data(p.data)
-        try:
+        decoder.push_end_of_frame()
+        
+        while decoder.get_number_of_input_bytes_pending() > 0:
             decoder.decode()
-
-        except libde265.WaitingForInput:
-            pass
-    
-    def _decode_hevc(self):
-        for stream, decoder in self._decoders.items():
-            try:
-                decoder.decode()
-
-            except libde265.WaitingForInput:
-                pass
-
+        
+        img = None
+        while img is None:
             img = decoder.get_next_picture()
-            if img is not None:
-                self._frames[stream] = _ycrcb2rgb(img)
+        
+        self._frames[k] = _ycrcb2rgb(img)
 
     def _flush_decoders(self):
         for decoder in self._decoders.values():
@@ -255,51 +171,43 @@ class FTLStream:
         Reads data for until the next timestamp. Returns False if there is no
         more data to read, otherwise returns True.
         '''
-        if self._packets_read == 0:
+
+        try:
             self._sp, self._p = self._read_next()
             self._packets_read += 1
-            
-        self._frames = {}
         
-        self._ts = self._sp.timestamp
-        ex = []
+        except msgpack.OutOfData:
+            return False
         
-        while self._sp.timestamp == self._ts:
-            # TODO: Can source be removed?
+        self._ts = self._sp.timestamp
             
-            try:
-                if self._p.codec == _codec_t.JSON:
-                    self._process_json(self._sp, self._p)
+        if self._p.block_total != 1 or self._p.block_number != 0:
+            raise Exception("Unsupported block format (todo)")
 
-                elif self._p.codec == _codec_t.CALIBRATION:
-                    self._update_calib(self._sp, self._p)
+        if self._p.codec == ftl.codec_t.JSON:
+            self._process_json(self._sp, self._p)
 
-                elif self._p.codec == _codec_t.POSE:
-                    self._update_pose(self._sp, self._p)
+        elif self._p.codec == ftl.codec_t.CALIBRATION:
+            self._update_calib(self._sp, self._p)
 
-                elif self._p.codec == _codec_t.HEVC:
-                    self._push_data_hevc(self._sp, self._p)
+        elif self._p.codec == ftl.codec_t.POSE:
+            self._update_pose(self._sp, self._p)
 
-                else:
-                    raise ValueError("unkowno codec %i" % self._p.codec)
-            
-            except Exception as e:
-                ex.append(e)
-            
-            try:
-                self._sp, self._p = self._read_next()
-                self._packets_read += 1
-            
-            except msgpack.OutOfData:
-                return False
-            
-        if len(ex) > 0:
-            raise Exception(ex)
-        
-        self._decode_hevc()
+        elif self._p.codec == ftl.codec_t.HEVC:
+            self._decode_hevc(self._sp, self._p)
 
+        else:
+            raise Exception("unkowno codec %i" % self._p.codec)
+
+        print("read() took: %.4f" % (t_end - t_start))
         return True
-    
+
+    def get_packet_count(self):
+        return self._packets_read
+
+    def get_raw(self):
+        return self._sp, self._p
+
     def get_timestamp(self):
         return self._ts
 
@@ -310,6 +218,8 @@ class FTLStream:
             raise ValueError("source id %i not found" % source)
 
     def get_camera_matrix(self, source):
+        ''' Camera intrinsic parameters '''
+
         calib = self.get_calibration(source)
         K = np.identity(3, dtype=np.float64)
         K[0,0] = calib.fx
@@ -325,7 +235,7 @@ class FTLStream:
             raise ValueError("source id %i not found" % source)
 
     def get_frames(self):
-        ''' Returns all frames '''
+        ''' All frames '''
         return self._frames
     
     def get_frame(self, source, channel):
@@ -336,9 +246,19 @@ class FTLStream:
             # raise an exception instead?
             return None
 
+    def get_Q(self, source):
+        ''' Disparity to depth matrix in OpenCV format '''
+
+        calib = self.get_calibration(source)
+        Q = np.identity(4, dtype=np.float64)
+        Q[0,3] = calib.cx
+        Q[1,3] = calib.cy
+        Q[2,2] = 0.0
+        Q[2,3] = calib.fx
+        Q[3,2] = -1 / calib.baseline
+        Q[3,3] = calib.doff
+        return Q
+
     def get_sources(self):
-        ''' Get list of sources
-        
-        todo: Is there a better way?
-        '''
+        ''' Get list of sources '''
         return list(self._calibration.keys())
\ No newline at end of file
diff --git a/python/ftl/ftltypes.py b/python/ftl/ftltypes.py
new file mode 100644
index 0000000000000000000000000000000000000000..b72f5606c15fcbcb63f30d90f54d0d59dd25fa2a
--- /dev/null
+++ b/python/ftl/ftltypes.py
@@ -0,0 +1,70 @@
+
+from collections import namedtuple
+from enum import IntEnum
+
+# components/rgbd-sources/include/ftl/rgbd/camera.hpp
+Camera = namedtuple("Camera", ["fx", "fy", "cx", "cy", "width", "height",
+                                "min_depth", "max_depth", "baseline", "doff"])
+
+# components/codecs/include/ftl/codecs/packet.hpp
+Packet = namedtuple("Packet", ["codec", "definition", "block_total",
+                               "block_number", "flags", "data"])
+
+StreamPacket = namedtuple("StreamPacket", ["timestamp", "streamID",
+                                           "chanel_count", "channel"])
+
+# components/codecs/include/ftl/codecs/channels.hpp
+class Channel(IntEnum):
+    None_           = -1
+    Colour          = 0
+    Left            = 0
+    Depth           = 1
+    Right           = 2
+    Colour2         = 2
+    Disparity       = 3
+    Depth2          = 3
+    Deviation       = 4
+    Normals         = 5
+    Points          = 6
+    Confidence      = 7
+    Contribution    = 7
+    EnergyVector    = 8
+    Flow            = 9
+    Energy          = 10
+    Mask            = 11
+    Density         = 12
+    LeftGray        = 13
+    RightGray       = 14
+    Overlay1        = 15
+
+    AudioLeft       = 32
+    AudioRight      = 33
+
+    Configuration   = 64
+    Calibration     = 65
+    Pose            = 66
+    Data            = 67
+
+# components/codecs/include/ftl/codecs/bitrates.hpp
+class codec_t(IntEnum):
+    JPG = 0
+    PNG = 1
+    H264 = 2
+    HEVC = 3
+    WAV = 4
+    JSON = 100
+    CALIBRATION = 101
+    POSE = 102
+    RAW = 103
+
+definition_t = {
+    0 : (7680, 4320),
+    1 : (2160, 3840),
+    2 : (1080, 1920),
+    3 : (720, 1280),
+    4 : (576, 1024),
+    5 : (480, 854),
+    6 : (360, 640),
+    7 : (0, 0),
+    8 : (2056, 1852)
+}
diff --git a/python/ftl/libde265.py b/python/ftl/libde265.py
index 5cab9f0cc5deaea6ac63c49cab7218073be17970..afa663640191b2b2ec64e4b5ab04c85ad5ea575a 100644
--- a/python/ftl/libde265.py
+++ b/python/ftl/libde265.py
@@ -29,11 +29,15 @@ import numpy as np
 
 import os 
 
+'''
 # default number of worker threads for decoder: half of os.cpu_count()
 
 _threads = os.cpu_count() // 2
 if _threads is None:
     _threads = 1
+'''
+
+_threads = 1
 
 # error codes copied from header (de265.h)
 
diff --git a/python/ftl/misc.py b/python/ftl/misc.py
new file mode 100644
index 0000000000000000000000000000000000000000..791dccf141d8129a03b1221f524fef03d0d96b42
--- /dev/null
+++ b/python/ftl/misc.py
@@ -0,0 +1,92 @@
+
+def disparity_to_depth(disparity, camera):
+    ''' Calculate depth map from disparity map '''
+    return (camera.fx * camera.baseline) / (disparity - camera.doff)
+
+from enum import IntEnum
+
+# components/codecs/include/ftl/codecs/hevc.hpp
+class NALType(IntEnum):
+    CODED_SLICE_TRAIL_N = 0
+    CODED_SLICE_TRAIL_R = 1
+
+    CODED_SLICE_TSA_N = 2
+    CODED_SLICE_TSA_R = 3
+
+    CODED_SLICE_STSA_N = 4
+    CODED_SLICE_STSA_R = 5
+
+    CODED_SLICE_RADL_N = 6
+    CODED_SLICE_RADL_R = 7
+
+    CODED_SLICE_RASL_N = 8
+    CODED_SLICE_RASL_R = 9
+
+    RESERVED_VCL_N10 = 10
+    RESERVED_VCL_R11 = 11
+    RESERVED_VCL_N12 = 12
+    RESERVED_VCL_R13 = 13
+    RESERVED_VCL_N14 = 14
+    RESERVED_VCL_R15 = 15
+
+    CODED_SLICE_BLA_W_LP = 16
+    CODED_SLICE_BLA_W_RADL = 17
+    CODED_SLICE_BLA_N_LP = 18
+    CODED_SLICE_IDR_W_RADL = 19
+    CODED_SLICE_IDR_N_LP = 20
+    CODED_SLICE_CRA = 21
+    RESERVED_IRAP_VCL22 = 22
+    RESERVED_IRAP_VCL23 = 23
+
+    RESERVED_VCL24 = 24
+    RESERVED_VCL25 = 25
+    RESERVED_VCL26 = 26
+    RESERVED_VCL27 = 27
+    RESERVED_VCL28 = 28
+    RESERVED_VCL29 = 29
+    RESERVED_VCL30 = 30
+    RESERVED_VCL31 = 31
+
+    VPS = 32
+    SPS = 33
+    PPS = 34
+    ACCESS_UNIT_DELIMITER = 35
+    EOS = 36
+    EOB = 37
+    FILLER_DATA = 38
+    PREFIX_SEI = 39
+    SUFFIX_SEI = 40
+
+    RESERVED_NVCL41 = 41
+    RESERVED_NVCL42 = 42
+    RESERVED_NVCL43 = 43
+    RESERVED_NVCL44 = 44
+    RESERVED_NVCL45 = 45
+    RESERVED_NVCL46 = 46
+    RESERVED_NVCL47 = 47
+    UNSPECIFIED_48 = 48
+    UNSPECIFIED_49 = 49
+    UNSPECIFIED_50 = 50
+    UNSPECIFIED_51 = 51
+    UNSPECIFIED_52 = 52
+    UNSPECIFIED_53 = 53
+    UNSPECIFIED_54 = 54
+    UNSPECIFIED_55 = 55
+    UNSPECIFIED_56 = 56
+    UNSPECIFIED_57 = 57
+    UNSPECIFIED_58 = 58
+    UNSPECIFIED_59 = 59
+    UNSPECIFIED_60 = 60
+    UNSPECIFIED_61 = 61
+    UNSPECIFIED_62 = 62
+    UNSPECIFIED_63 = 63
+    INVALID = 64
+
+def get_NAL_type(data):
+    if not isinstance(data, bytes):
+        raise ValueError("expected bytes")
+    
+    return NALType((data[4] >> 1) & 0x3f)
+
+def is_iframe(data):
+    return get_NAL_type(data) == NALType.VPS