diff --git a/Cargo.lock b/Cargo.lock
index 510aef4c960f1e020263407ccdfff02826587d6b..8596e43a96453fe5659bb3fa2da29f60bc932d36 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -382,6 +382,12 @@ dependencies = [
  "adler32",
 ]
 
+[[package]]
+name = "itoa"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e"
+
 [[package]]
 name = "jpeg-decoder"
 version = "0.1.18"
@@ -450,6 +456,12 @@ dependencies = [
  "rustc_version",
 ]
 
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
 [[package]]
 name = "miniz_oxide"
 version = "0.3.6"
@@ -698,6 +710,12 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "ryu"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8"
+
 [[package]]
 name = "scoped_threadpool"
 version = "0.1.9"
@@ -725,6 +743,37 @@ version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
 
+[[package]]
+name = "serde"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9371ade75d4c2d6cb154141b9752cf3781ec9c05e0e5cf35060e1e70ee7b9c25"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
 [[package]]
 name = "shlex"
 version = "0.1.1"
@@ -809,6 +858,9 @@ dependencies = [
  "fraction",
  "image",
  "log",
+ "mime",
+ "serde",
+ "serde_json",
  "time",
 ]
 
diff --git a/Cargo.toml b/Cargo.toml
index 094af427155cb12f45e918fe685aed5b954af049..35ac6cf6e322806c5b89b226a810914e532456db 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,4 +13,7 @@ image = "0.23.0"
 enum_primitive = "0.1.1"
 failure = "0.1.6"
 fraction = "0.6.2"
-time = "0.2"
\ No newline at end of file
+mime = "0.3.16"
+time = "0.2"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
\ No newline at end of file
diff --git a/src/ffmpeg_api/api.rs b/src/ffmpeg_api/api.rs
index f166b40390f5f82372b2c8aff94bec81aaff4f7e..cf6b7eae916f1d96374c6f2f48f3ff7a05c70bd6 100644
--- a/src/ffmpeg_api/api.rs
+++ b/src/ffmpeg_api/api.rs
@@ -1,11 +1,13 @@
-use ffmpeg_dev::sys as ffi;
-use failure::bail;
-use enum_primitive::*;
 use std::marker::PhantomData;
+
+use enum_primitive::*;
+use failure::bail;
+use ffmpeg_dev::sys as ffi;
 use fraction::Fraction;
 
 use crate::ffmpeg_api::enums::*;
 use crate::util::media_time;
+use std::path::Path;
 
 pub struct AVFormatContext {
     base: *mut ffi::AVFormatContext,
@@ -20,13 +22,16 @@ impl<'a> AVFormatContext {
         Ok(AVFormatContext { base })
     }
 
-    pub fn open_input(&mut self, path: &str) -> Result<(), failure::Error> {
+    pub fn open_input(&mut self, path: &Path) -> Result<(), failure::Error> {
+        let path = path.to_str()
+            .ok_or_else(|| failure::format_err!("Could not convert path to c string"))?;
+        let path = std::ffi::CString::new(path)
+            .map_err(|_| failure::format_err!("Could not convert path to c string"))?;
+
         match unsafe {
             ffi::avformat_open_input(
                 &mut self.base,
-                std::ffi::CString::new(path)
-                    .map_err(|_| failure::format_err!("Could not convert path to c string"))?
-                    .as_ptr(),
+                path.as_ptr(),
                 std::ptr::null_mut(),
                 std::ptr::null_mut(),
             )
@@ -36,6 +41,10 @@ impl<'a> AVFormatContext {
         }
     }
 
+    pub fn input_format(&self) -> AVInputFormat {
+        AVInputFormat::new(unsafe { (*self.base).iformat.as_mut() }.expect("not null"))
+    }
+
     pub fn streams(&self) -> Vec<AVStream> {
         Vec::from(
             unsafe {
@@ -69,6 +78,15 @@ impl<'a> AVFormatContext {
             errno => Err(failure::format_err!("Error while decoding frame: {}", errno))
         }
     }
+
+    pub fn duration(&self) -> Result<media_time::MediaTime, failure::Error> {
+        media_time::MediaTime::from_rational(
+            unsafe { (*self.base).duration }, Fraction::new(
+                1 as u64,
+                ffi::AV_TIME_BASE as u64,
+            ),
+        )
+    }
 }
 
 impl Drop for AVFormatContext {
@@ -77,6 +95,89 @@ impl Drop for AVFormatContext {
     }
 }
 
+pub struct AVInputFormat<'a> {
+    base: &'a mut ffi::AVInputFormat
+}
+
+impl<'a> AVInputFormat<'a> {
+    fn new(base: &'a mut ffi::AVInputFormat) -> Self {
+        return AVInputFormat { base };
+    }
+
+    pub fn long_name(&self) -> Result<String, failure::Error> {
+        let raw: *const ::std::os::raw::c_char = self.base.long_name;
+
+        if raw.is_null() {
+            Err(failure::format_err!("No mime type found"))
+        } else {
+            Ok(String::from(unsafe {
+                std::ffi::CStr::from_ptr(raw)
+            }.to_str().map_err(|err| {
+                failure::format_err!("Could not convert mime type to string: {}", err)
+            })?))
+        }
+    }
+
+    pub fn name(&self) -> Result<String, failure::Error> {
+        let raw: *const ::std::os::raw::c_char = self.base.name;
+
+        if raw.is_null() {
+            Err(failure::format_err!("No mime type found"))
+        } else {
+            Ok(String::from(unsafe {
+                std::ffi::CStr::from_ptr(raw)
+            }.to_str().map_err(|err| {
+                failure::format_err!("Could not convert mime type to string: {}", err)
+            })?))
+        }
+    }
+
+    pub fn mime(&self) -> Result<String, failure::Error> {
+        let raw: *const ::std::os::raw::c_char = self.base.mime_type;
+
+        if raw.is_null() {
+            Err(failure::format_err!("No mime type found"))
+        } else {
+            Ok(String::from(unsafe {
+                std::ffi::CStr::from_ptr(raw)
+            }.to_str().map_err(|err| {
+                failure::format_err!("Could not convert mime type to string: {}", err)
+            })?))
+        }
+    }
+
+    pub fn determine_mime<T: AsRef<str>>(&self, stream_codec: T) -> Result<String, failure::Error> {
+        let containers = self.name()?;
+        let stream_codec = stream_codec.as_ref();
+
+        for container in containers.split(",") {
+            match container {
+                "mp4" => match stream_codec {
+                    "h264" => {
+                        return Ok(String::from("video/mp4"));
+                    }
+                    _ => {
+                        // Do nothing
+                    }
+                }
+                "webm" => match stream_codec {
+                    "vp8" | "vp9" | "av1" => {
+                        return Ok(String::from("video/webm"));
+                    }
+                    _ => {
+                        // Do nothing
+                    }
+                }
+                _ => {
+                    // Do nothing
+                }
+            }
+        }
+
+        return Err(failure::format_err!("Could not determine mime type: {} video in {} container", stream_codec, containers));
+    }
+}
+
 pub struct AVBuffer {
     base: *mut u8,
     size: usize,
@@ -349,6 +450,10 @@ impl<'a> AVCodecParameters<'a> {
         AVCodecID::from_u32(self.base.codec_id)
     }
 
+    pub fn bit_rate(&self) -> i64 {
+        self.base.bit_rate
+    }
+
     pub fn find_decoder(&self) -> AVCodec {
         AVCodec::new(
             unsafe { ffi::avcodec_find_decoder(self.base.codec_id).as_mut() }.expect("Decoder not found"),
diff --git a/src/thumbnail/extract.rs b/src/ingest/extract.rs
similarity index 84%
rename from src/thumbnail/extract.rs
rename to src/ingest/extract.rs
index 0086d2c91fd377e3c5525cf0e4ef10991ac804fd..9c610e6b723f57c1ede6e4226346c5957bd7d6c5 100644
--- a/src/thumbnail/extract.rs
+++ b/src/ingest/extract.rs
@@ -1,29 +1,34 @@
+use std::path::Path;
+use failure::format_err;
+
 use crate::ffmpeg_api::api::*;
 use crate::ffmpeg_api::enums::*;
 use crate::util::media_time::*;
-use crate::thumbnail::spritesheet::*;
-
-use failure::format_err;
+use crate::ingest::spritesheet::*;
+use crate::util::stream_metadata::*;
 
-pub fn extract<T: AsRef<str>, U: AsRef<str>>(
+pub fn extract(
     max_size: u32,
     num_horizontal: u32, num_vertical: u32,
     frame_interval: MediaTime,
-    input_file: T,
-    output_folder: U,
+    input_file: &Path,
+    output_folder: &Path,
 ) -> Result<(), failure::Error> {
     let mut avformat_context = AVFormatContext::new().map_err(|error| {
         format_err!("Could not allocate a context to process the video: {:?}", error)
     })?;
-    avformat_context.open_input(input_file.as_ref()).map_err(|error| {
+    avformat_context.open_input(input_file).map_err(|error| {
         format_err!("Could not open video input: {:?}", error)
     })?;
+    let duration = avformat_context.duration()?;
 
+    let spritesheet_path = output_folder.join("spritesheets");
+    std::fs::create_dir_all(&spritesheet_path)?;
     let mut spritesheet_manager = SpritesheetManager::new(
         max_size,
         num_horizontal, num_vertical,
         frame_interval,
-        &output_folder,
+        spritesheet_path,
         "preview"
     );
 
@@ -36,7 +41,6 @@ pub fn extract<T: AsRef<str>, U: AsRef<str>>(
 
     let index = stream.index();
     let time_base = stream.time_base();
-    let duration = stream.duration()?;
 
     let codec_parameters = stream.codec_parameters();
     let local_codec = codec_parameters.find_decoder();
@@ -48,6 +52,12 @@ pub fn extract<T: AsRef<str>, U: AsRef<str>>(
         local_codec.name()
     );
 
+    let mut metadata = StreamMetadata::new(
+        avformat_context.input_format().determine_mime(local_codec.name())?,
+        duration,
+        codec_parameters.bit_rate() / 1000
+    );
+
     let mut output_frame = AVFrame::new().map_err(|error| {
         format_err!("Could not create output frame: {:?}", error)
     })?;
@@ -91,6 +101,7 @@ pub fn extract<T: AsRef<str>, U: AsRef<str>>(
                     if spritesheet_manager.fulfils_frame_interval(timestamp) {
                         if !spritesheet_manager.initialized() {
                             spritesheet_manager.initialize(frame.width() as u32, frame.height() as u32);
+                            metadata.set_frame_size(frame.width(), frame.height());
                             output_frame.init(
                                 spritesheet_manager.sprite_width() as i32,
                                 spritesheet_manager.sprite_height() as i32,
@@ -128,5 +139,11 @@ pub fn extract<T: AsRef<str>, U: AsRef<str>>(
         spritesheet_manager.save()?;
     }
 
+    metadata.save(
+        output_folder.join("metadata.json")
+    ).map_err(|error| {
+        format_err!("Could not write stream metadata: {}", error)
+    })?;
+
     Ok(())
 }
\ No newline at end of file
diff --git a/src/thumbnail/mod.rs b/src/ingest/mod.rs
similarity index 100%
rename from src/thumbnail/mod.rs
rename to src/ingest/mod.rs
diff --git a/src/thumbnail/spritesheet.rs b/src/ingest/spritesheet.rs
similarity index 89%
rename from src/thumbnail/spritesheet.rs
rename to src/ingest/spritesheet.rs
index 251664b7c171da73c5fc763d54e791bbb9be011d..e0e05b806cfbb0d000b72fa96c0ec4968c215e26 100644
--- a/src/thumbnail/spritesheet.rs
+++ b/src/ingest/spritesheet.rs
@@ -1,3 +1,5 @@
+use std::path::PathBuf;
+
 use image::{RgbImage, ImageBuffer};
 use failure::{bail, format_err};
 
@@ -15,13 +17,13 @@ pub struct SpritesheetManager {
     last_timestamp: MediaTime,
     frame_interval: MediaTime,
     metadata: WebVTTFile,
-    output_path: std::string::String,
+    output_path: PathBuf,
     name: std::string::String,
     initialized: bool,
 }
 
 impl SpritesheetManager {
-    pub fn new<T: AsRef<str>, U: AsRef<str>>(max_side: u32, num_horizontal: u32, num_vertical: u32, frame_interval: MediaTime, output_path: T, name: U) -> SpritesheetManager {
+    pub fn new<T: AsRef<str>, U: Into<PathBuf>>(max_side: u32, num_horizontal: u32, num_vertical: u32, frame_interval: MediaTime, output_path: U, name: T) -> SpritesheetManager {
         SpritesheetManager {
             num_horizontal,
             num_vertical,
@@ -33,7 +35,7 @@ impl SpritesheetManager {
             last_timestamp: MediaTime::from_millis(0),
             frame_interval,
             metadata: WebVTTFile::new(),
-            output_path: std::string::String::from(output_path.as_ref()),
+            output_path: output_path.into(),
             name: std::string::String::from(name.as_ref()),
             initialized: false,
         }
@@ -141,7 +143,7 @@ impl SpritesheetManager {
 
     fn save_spritesheet(&mut self) -> Result<(), failure::Error> {
         self.spritesheet.save(
-            format!("{}/{}_{}.jpg", self.output_path, self.name, self.spritesheet_index(self.current_image))
+            self.output_path.join(format!("{}_{}.jpg", self.name, self.spritesheet_index(self.current_image)))
         ).map_err(|error| {
             format_err!("Could not write spritesheet: {}", error)
         })?;
@@ -151,7 +153,9 @@ impl SpritesheetManager {
 
     pub fn save(&mut self) -> Result<(), failure::Error> {
         self.save_spritesheet()?;
-        self.metadata.save(format!("{}/{}.vtt", self.output_path, self.name)).map_err(|error| {
+        self.metadata.save(
+            self.output_path.join(format!("{}.vtt", self.name))
+        ).map_err(|error| {
             format_err!("Could not write spritesheet metadata: {}", error)
         })?;
         Ok(())
diff --git a/src/main.rs b/src/main.rs
index b6c2e49bde5a5128dd848a72fe41eca76b7df496..d8ce00983baa405e5e74dddf2da5e161000f939f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,17 +1,18 @@
 #![allow(dead_code)]
 
 pub(crate) mod ffmpeg_api;
-pub(crate) mod thumbnail;
+pub(crate) mod ingest;
 pub(crate) mod util;
 
 use crate::util::media_time::MediaTime;
+use std::path::Path;
 
 fn main() -> Result<(), failure::Error> {
-    thumbnail::extract::extract(
+    ingest::extract::extract(
         160, 5, 5,
         MediaTime::from_seconds(2),
-        "/home/janne/Workspace/justflix/data/video.mp4",
-        "/home/janne/Workspace/justflix/data/spritesheets"
+        Path::new("/home/kuschku/Workspace/projects/mediaflix/data/movie.mp4"),
+        Path::new("/home/kuschku/Workspace/projects/mediaflix/data/output")
     )?;
 
     Ok(())
diff --git a/src/util/media_time.rs b/src/util/media_time.rs
index 34396093f2d48336cfc36e956df2b04dfa6de03d..54bd366ce68a807a25f81cb2833263516bf2f94a 100644
--- a/src/util/media_time.rs
+++ b/src/util/media_time.rs
@@ -14,21 +14,29 @@ impl MediaTime {
         })?;
 
         Ok(MediaTime(time::Duration::milliseconds(
-            1000 * timestamp * num as i64 / den as i64
+            (1000 * timestamp as i128 * num as i128 / den as i128) as i64
         )))
     }
 
+    #[inline(always)]
     pub fn from_millis(timestamp: i64) -> MediaTime {
         MediaTime(time::Duration::milliseconds(timestamp))
     }
 
+    #[inline(always)]
     pub fn from_seconds(timestamp: i64) -> MediaTime {
         MediaTime(time::Duration::seconds(timestamp))
     }
 
+    #[inline(always)]
     pub fn is_zero(&self) -> bool {
         self.0.is_zero()
     }
+
+    #[inline(always)]
+    pub fn seconds(&self) -> i64 {
+        self.0.whole_seconds()
+    }
 }
 
 impl std::fmt::Display for MediaTime {
diff --git a/src/util/mod.rs b/src/util/mod.rs
index f6c6f2a78a72b7245ce465a7c047a29dcff45608..28d153216d4d47d35355a14b65f6104ee04866b5 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -1,2 +1,3 @@
+pub(crate) mod media_time;
+pub(crate) mod stream_metadata;
 pub(crate) mod webvtt;
-pub(crate) mod media_time;
\ No newline at end of file
diff --git a/src/util/stream_metadata.rs b/src/util/stream_metadata.rs
new file mode 100644
index 0000000000000000000000000000000000000000..35e1412844529367fac1bced5553f639015edf2b
--- /dev/null
+++ b/src/util/stream_metadata.rs
@@ -0,0 +1,41 @@
+use std::fs::File;
+use std::path::Path;
+use std::io::BufWriter;
+
+use serde::{Deserialize, Serialize};
+
+use crate::util::media_time::MediaTime;
+
+#[derive(Serialize, Deserialize)]
+pub struct StreamMetadata {
+    content_type: String,
+    duration: i64,
+    bitrate: i64,
+    aspect_ratio: f32,
+    width: i32,
+    height: i32,
+}
+
+impl StreamMetadata {
+    pub fn new<T: AsRef<str>>(content_type: T, duration: MediaTime, bitrate: i64) -> StreamMetadata {
+        StreamMetadata {
+            content_type: String::from(content_type.as_ref()),
+            duration: duration.seconds(),
+            bitrate,
+            aspect_ratio: 0.0,
+            width: 0,
+            height: 0,
+        }
+    }
+
+    pub fn set_frame_size(&mut self, width: i32, height: i32) {
+        self.width = width;
+        self.height = height;
+        self.aspect_ratio = (width as f64 / height as f64) as f32;
+    }
+
+    pub fn save<T: AsRef<Path>>(&self, path: T) -> Result<(), std::io::Error> {
+        serde_json::to_writer(BufWriter::new(File::create(path)?), self)?;
+        Ok(())
+    }
+}
\ No newline at end of file
diff --git a/src/util/webvtt.rs b/src/util/webvtt.rs
index 64f56034c7b0b92efbee8ff28ee9ea316cbb6790..c26b8e9fd6ad343451fbb5f586518f2627394071 100644
--- a/src/util/webvtt.rs
+++ b/src/util/webvtt.rs
@@ -1,6 +1,8 @@
 use std::fs::File;
 use std::io::prelude::*;
 use std::io::LineWriter;
+use std::path::Path;
+use std::string::String;
 
 use crate::util::media_time::MediaTime;
 
@@ -11,7 +13,7 @@ pub struct WebVTTFile {
 pub struct WebVTTCue {
     start: MediaTime,
     end: MediaTime,
-    payload: std::string::String,
+    payload: String,
 }
 
 impl WebVTTFile {
@@ -25,7 +27,7 @@ impl WebVTTFile {
         self.cues.push(cue);
     }
 
-    pub fn save(&self, path: std::string::String) -> Result<(), std::io::Error> {
+    pub fn save<T: AsRef<Path>>(&self, path: T) -> Result<(), std::io::Error> {
         let file = File::create(path)?;
         let mut file = LineWriter::new(file);
         file.write_all(b"WEBVTT\n\n")?;
@@ -38,7 +40,7 @@ impl WebVTTFile {
 }
 
 impl WebVTTCue {
-    pub fn new(start: MediaTime, end: MediaTime, payload: std::string::String) -> WebVTTCue {
+    pub fn new(start: MediaTime, end: MediaTime, payload: String) -> WebVTTCue {
         WebVTTCue { start, end, payload }
     }