diff --git a/src/media/audio/audio_rtp_session.cpp b/src/media/audio/audio_rtp_session.cpp
index edb7d7b4b74dd30649999c8a6eb28c71342b71bf..eb1f779a89610cad0b03479b66343bfa04eb8fc0 100644
--- a/src/media/audio/audio_rtp_session.cpp
+++ b/src/media/audio/audio_rtp_session.cpp
@@ -118,9 +118,9 @@ AudioSender::setup(SocketPair& socketPair)
 
     try {
         /* Encoder setup */
-        RING_DBG("audioEncoder_->openOutput %s", dest_.c_str());
+        RING_DBG("audioEncoder_->openLiveOutput %s", dest_.c_str());
         audioEncoder_->setMuted(muteState_);
-        audioEncoder_->openOutput(dest_, args_);
+        audioEncoder_->openLiveOutput(dest_, args_);
         audioEncoder_->setInitSeqVal(seqVal_);
         audioEncoder_->setIOContext(muxContext_);
         audioEncoder_->startIO();
diff --git a/src/media/media_encoder.cpp b/src/media/media_encoder.cpp
index 7a0b5fe51334c5c8b9f52fbb89e462430cf14f85..7207c862d49bcacc8aafb969786c1de857eb5187 100644
--- a/src/media/media_encoder.cpp
+++ b/src/media/media_encoder.cpp
@@ -29,6 +29,10 @@
 #include "string_utils.h"
 #include "logger.h"
 
+extern "C" {
+#include <libavutil/parseutils.h>
+}
+
 #include <iostream>
 #include <sstream>
 #include <algorithm>
@@ -49,8 +53,9 @@ MediaEncoder::~MediaEncoder()
         if (outputCtx_->priv_data)
             av_write_trailer(outputCtx_);
 
-        if (encoderCtx_)
-            avcodec_close(encoderCtx_);
+        for (auto encoderCtx : encoders_)
+            if (encoderCtx)
+                avcodec_close(encoderCtx);
 
         avformat_free_context(outputCtx_);
     }
@@ -119,12 +124,12 @@ MediaEncoder::getLastSeqValue()
 std::string
 MediaEncoder::getEncoderName() const
 {
-    return encoderCtx_->codec->name;
+    return encoders_[currentStreamIdx_]->codec->name;
 }
 
 void
-MediaEncoder::openOutput(const std::string& filename,
-                         const ring::MediaDescription& args)
+MediaEncoder::openLiveOutput(const std::string& filename,
+                             const ring::MediaDescription& args)
 {
     setOptions(args);
     AVOutputFormat *oformat = av_guess_format("rtp", filename.c_str(), nullptr);
@@ -136,7 +141,7 @@ MediaEncoder::openOutput(const std::string& filename,
 
     outputCtx_->oformat = oformat;
 #if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 7, 100)
-    // c_str guarantees NULL termination
+    // c_str guarantees null termination
     outputCtx_->url = av_strdup(filename.c_str()); // must be compatible with av_free
 #else
     strncpy(outputCtx_->filename, filename.c_str(), sizeof(outputCtx_->filename));
@@ -144,98 +149,129 @@ MediaEncoder::openOutput(const std::string& filename,
     outputCtx_->filename[sizeof(outputCtx_->filename) - 1] = '\0';
 #endif
 
+    addStream(args.codec->systemCodecInfo, args.parameters);
+}
+
+void
+MediaEncoder::openFileOutput(const std::string& filename, std::map<std::string, std::string> options)
+{
+    avformat_free_context(outputCtx_);
+    avformat_alloc_output_context2(&outputCtx_, nullptr, nullptr, filename.c_str());
+    auto bitrate = SystemCodecInfo::DEFAULT_MAX_BITRATE;
+    auto quality = SystemCodecInfo::DEFAULT_CODEC_QUALITY;
+    // ensure all options retrieved later on are in options_ (insert does nothing if key exists)
+    options.insert({"max_rate", ring::to_string(bitrate)});
+    options.insert({"crf", ring::to_string(quality)});
+    options.insert({"sample_rate", "8000"});
+    options.insert({"channels", "2"});
+    int sampleRate = atoi(options["sample_rate"].c_str());
+    options.insert({"frame_size", ring::to_string(static_cast<unsigned>(0.02*sampleRate))});
+    options.insert({"width", "320"});
+    options.insert({"height", "240"});
+    options.insert({"framerate", "30"});
+    for (const auto& it : options)
+        av_dict_set(&options_, it.first.c_str(), it.second.c_str(), 0);
+    // for a file output, addStream is done by the caller, as there may be multiple streams
+}
+
+int
+MediaEncoder::addStream(const SystemCodecInfo& systemCodecInfo, std::string parameters)
+{
+    AVCodec* outputCodec = nullptr;
+    AVCodecContext* encoderCtx = nullptr;
     /* find the video encoder */
-    if (args.codec->systemCodecInfo.avcodecId == AV_CODEC_ID_H263)
+    if (systemCodecInfo.avcodecId == AV_CODEC_ID_H263)
         // For H263 encoding, we force the use of AV_CODEC_ID_H263P (H263-1998)
         // H263-1998 can manage all frame sizes while H263 don't
         // AV_CODEC_ID_H263 decoder will be used for decoding
-        outputEncoder_ = avcodec_find_encoder(AV_CODEC_ID_H263P);
+        outputCodec = avcodec_find_encoder(AV_CODEC_ID_H263P);
     else
-        outputEncoder_ = avcodec_find_encoder((AVCodecID)args.codec->systemCodecInfo.avcodecId);
-    if (!outputEncoder_) {
-        RING_ERR("Encoder \"%s\" not found!", args.codec->systemCodecInfo.name.c_str());
+        outputCodec = avcodec_find_encoder(static_cast<AVCodecID>(systemCodecInfo.avcodecId));
+    if (!outputCodec) {
+        RING_ERR("Encoder \"%s\" not found!", systemCodecInfo.name.c_str());
         throw MediaEncoderException("No output encoder");
     }
 
-    prepareEncoderContext(args.codec->systemCodecInfo.mediaType == MEDIA_VIDEO);
-    auto maxBitrate = 1000 * atoi(av_dict_get(options_, "max_rate", NULL, 0)->value);
+    encoderCtx = prepareEncoderContext(outputCodec, systemCodecInfo.mediaType == MEDIA_VIDEO);
+    encoders_.push_back(encoderCtx);
+    auto maxBitrate = 1000 * atoi(av_dict_get(options_, "max_rate", nullptr, 0)->value);
     auto bufSize = 2 * maxBitrate; // as recommended (TODO: make it customizable)
-    auto crf = atoi(av_dict_get(options_, "crf", NULL, 0)->value);
+    auto crf = atoi(av_dict_get(options_, "crf", nullptr, 0)->value);
 
     /* let x264 preset override our encoder settings */
-    if (args.codec->systemCodecInfo.avcodecId == AV_CODEC_ID_H264) {
-        extractProfileLevelID(args.parameters, encoderCtx_);
-        forcePresetX264();
+    if (systemCodecInfo.avcodecId == AV_CODEC_ID_H264) {
+        extractProfileLevelID(parameters, encoderCtx);
+        forcePresetX264(encoderCtx);
         // For H264 :
         // Streaming => VBV (constrained encoding) + CRF (Constant Rate Factor)
         if (crf == SystemCodecInfo::DEFAULT_NO_QUALITY)
             crf = 30; // good value for H264-720p@30
         RING_DBG("H264 encoder setup: crf=%u, maxrate=%u, bufsize=%u", crf, maxBitrate, bufSize);
 
-        av_opt_set(encoderCtx_->priv_data, "crf", av_dict_get(options_, "crf", NULL, 0)->value, 0);
-        encoderCtx_->rc_buffer_size = bufSize;
-        encoderCtx_->rc_max_rate = maxBitrate;
-    } else if (args.codec->systemCodecInfo.avcodecId == AV_CODEC_ID_VP8) {
+        av_opt_set(encoderCtx->priv_data, "crf", av_dict_get(options_, "crf", nullptr, 0)->value, 0);
+        encoderCtx->rc_buffer_size = bufSize;
+        encoderCtx->rc_max_rate = maxBitrate;
+    } else if (systemCodecInfo.avcodecId == AV_CODEC_ID_VP8) {
         // For VP8 :
         // 1- if quality is set use it
         // bitrate need to be set. The target bitrate becomes the maximum allowed bitrate
         // 2- otherwise set rc_max_rate and rc_buffer_size
         // Using information given on this page:
         // http://www.webmproject.org/docs/encoder-parameters/
-        av_opt_set(encoderCtx_->priv_data, "quality", "realtime", 0);
-        av_opt_set_int(encoderCtx_->priv_data, "error-resilient", 1, 0);
-        av_opt_set_int(encoderCtx_->priv_data, "cpu-used", 7, 0); // value obtained from testing
-        av_opt_set_int(encoderCtx_->priv_data, "lag-in-frames", 0, 0);
+        av_opt_set(encoderCtx->priv_data, "quality", "realtime", 0);
+        av_opt_set_int(encoderCtx->priv_data, "error-resilient", 1, 0);
+        av_opt_set_int(encoderCtx->priv_data, "cpu-used", 7, 0); // value obtained from testing
+        av_opt_set_int(encoderCtx->priv_data, "lag-in-frames", 0, 0);
         // allow encoder to drop frames if buffers are full and
         // to undershoot target bitrate to lessen strain on resources
-        av_opt_set_int(encoderCtx_->priv_data, "drop-frame", 25, 0);
-        av_opt_set_int(encoderCtx_->priv_data, "undershoot-pct", 95, 0);
-        // don't set encoderCtx_->gop_size: let libvpx decide when to insert a keyframe
-        encoderCtx_->slices = 2; // VP8E_SET_TOKEN_PARTITIONS
-        encoderCtx_->qmin = 4;
-        encoderCtx_->qmax = 56;
-        encoderCtx_->rc_buffer_size = maxBitrate;
-        encoderCtx_->bit_rate = maxBitrate;
+        av_opt_set_int(encoderCtx->priv_data, "drop-frame", 25, 0);
+        av_opt_set_int(encoderCtx->priv_data, "undershoot-pct", 95, 0);
+        // don't set encoderCtx->gop_size: let libvpx decide when to insert a keyframe
+        encoderCtx->slices = 2; // VP8E_SET_TOKEN_PARTITIONS
+        encoderCtx->qmin = 4;
+        encoderCtx->qmax = 56;
+        encoderCtx->rc_buffer_size = maxBitrate;
+        encoderCtx->bit_rate = maxBitrate;
         if (crf != SystemCodecInfo::DEFAULT_NO_QUALITY) {
-            av_opt_set(encoderCtx_->priv_data, "crf", av_dict_get(options_, "crf", NULL, 0)->value, 0);
+            av_opt_set(encoderCtx->priv_data, "crf", av_dict_get(options_, "crf", nullptr, 0)->value, 0);
             RING_DBG("Using quality factor %d", crf);
         } else {
             RING_DBG("Using Max bitrate %d", maxBitrate);
         }
-    } else if (args.codec->systemCodecInfo.avcodecId == AV_CODEC_ID_MPEG4) {
+    } else if (systemCodecInfo.avcodecId == AV_CODEC_ID_MPEG4) {
         // For MPEG4 :
         // No CRF avaiable.
         // Use CBR (set bitrate)
-        encoderCtx_->rc_buffer_size = maxBitrate;
-        encoderCtx_->bit_rate = encoderCtx_->rc_min_rate = encoderCtx_->rc_max_rate =  maxBitrate;
+        encoderCtx->rc_buffer_size = maxBitrate;
+        encoderCtx->bit_rate = encoderCtx->rc_min_rate = encoderCtx->rc_max_rate =  maxBitrate;
         RING_DBG("Using Max bitrate %d", maxBitrate);
-    } else if (args.codec->systemCodecInfo.avcodecId == AV_CODEC_ID_H263) {
-        encoderCtx_->bit_rate = encoderCtx_->rc_max_rate =  maxBitrate;
-        encoderCtx_->rc_buffer_size = maxBitrate;
+    } else if (systemCodecInfo.avcodecId == AV_CODEC_ID_H263) {
+        encoderCtx->bit_rate = encoderCtx->rc_max_rate =  maxBitrate;
+        encoderCtx->rc_buffer_size = maxBitrate;
         RING_DBG("Using Max bitrate %d", maxBitrate);
     }
 
-    int ret;
-    ret = avcodec_open2(encoderCtx_, outputEncoder_, NULL);
-    if (ret)
+    if (avcodec_open2(encoderCtx, outputCodec, nullptr) < 0)
         throw MediaEncoderException("Could not open encoder");
 
     // add video stream to outputformat context
-    stream_ = avformat_new_stream(outputCtx_, 0);
-    if (!stream_)
+    AVStream* stream = avformat_new_stream(outputCtx_, outputCodec);
+    if (!stream)
         throw MediaEncoderException("Could not allocate stream");
 
+    currentStreamIdx_ = stream->index;
+
 #ifndef _WIN32
-    avcodec_parameters_from_context(stream_->codecpar, encoderCtx_);
+    avcodec_parameters_from_context(stream->codecpar, encoderCtx);
 #else
-    stream_->codec = encoderCtx_;
+    stream->codec = encoderCtx;
 #endif
 #ifdef RING_VIDEO
-    if (args.codec->systemCodecInfo.mediaType == MEDIA_VIDEO) {
+    if (systemCodecInfo.mediaType == MEDIA_VIDEO) {
         // allocate buffers for both scaled (pre-encoder) and encoded frames
-        const int width = encoderCtx_->width;
-        const int height = encoderCtx_->height;
-        const int format = libav_utils::ring_pixel_format((int)encoderCtx_->pix_fmt);
+        const int width = encoderCtx->width;
+        const int height = encoderCtx->height;
+        const int format = libav_utils::ring_pixel_format((int)encoderCtx->pix_fmt);
         scaledFrameBufferSize_ = videoFrameSize(format, width, height);
         if (scaledFrameBufferSize_ <= AV_INPUT_BUFFER_MIN_SIZE)
             throw MediaEncoderException("buffer too small");
@@ -244,6 +280,8 @@ MediaEncoder::openOutput(const std::string& filename,
         scaledFrame_.setFromMemory(scaledFrameBuffer_.data(), format, width, height);
     }
 #endif // RING_VIDEO
+
+    return stream->index;
 }
 
 void MediaEncoder::setInterruptCallback(int (*cb)(void*), void *opaque)
@@ -258,14 +296,29 @@ void MediaEncoder::setInterruptCallback(int (*cb)(void*), void *opaque)
 
 void MediaEncoder::setIOContext(const std::unique_ptr<MediaIOHandle> &ioctx)
 {
-    outputCtx_->pb = ioctx->getContext();
-    outputCtx_->packet_size = outputCtx_->pb->buffer_size;
+    if (ioctx) {
+        outputCtx_->pb = ioctx->getContext();
+        outputCtx_->packet_size = outputCtx_->pb->buffer_size;
+    } else {
+        int ret = 0;
+#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 7, 100)
+        const char* filename = outputCtx_->url;
+#else
+        const char* filename = outputCtx_->filename;
+#endif
+        if (!(outputCtx_->oformat->flags & AVFMT_NOFILE)) {
+            if ((ret = avio_open(&outputCtx_->pb, filename, AVIO_FLAG_WRITE)) < 0) {
+                throw MediaEncoderException(
+                    std::string("Could not set IO context" + libav_utils::getError(ret)).c_str());
+            }
+        }
+    }
 }
 
 void
 MediaEncoder::startIO()
 {
-    if (avformat_write_header(outputCtx_, options_ ? &options_ : NULL)) {
+    if (avformat_write_header(outputCtx_, options_ ? &options_ : nullptr)) {
         RING_ERR("Could not write header for output file... check codec parameters");
         throw MediaEncoderException("Failed to write output file header");
     }
@@ -300,15 +353,13 @@ MediaEncoder::encode(VideoFrame& input, bool is_keyframe,
         frame->key_frame = 0;
     }
 
-    int ret = encode(frame, stream_->index);
-
-    return ret;
+    return encode(frame, currentStreamIdx_);
 }
 #endif // RING_VIDEO
 
 int MediaEncoder::encode_audio(const AudioBuffer &buffer)
 {
-    const int needed_bytes = av_samples_get_buffer_size(NULL, buffer.channels(),
+    const int needed_bytes = av_samples_get_buffer_size(nullptr, buffer.channels(),
                                                         buffer.frames(),
                                                         AV_SAMPLE_FMT_S16, 0);
     if (needed_bytes < 0) {
@@ -322,6 +373,8 @@ int MediaEncoder::encode_audio(const AudioBuffer &buffer)
     AudioSample *offset_ptr = sample_data;
     int nb_frames = buffer.frames();
 
+    AVCodecContext* encoderCtx = encoders_[currentStreamIdx_];
+
     if (not is_muted) {
         //only fill buffer with samples if not muted
         buffer.interleave(sample_data);
@@ -337,9 +390,9 @@ int MediaEncoder::encode_audio(const AudioBuffer &buffer)
         if (!frame)
             return -1;
 
-        if (encoderCtx_->frame_size)
+        if (encoderCtx->frame_size)
             frame->nb_samples = std::min<int>(nb_frames,
-                                              encoderCtx_->frame_size);
+                                              encoderCtx->frame_size);
         else
             frame->nb_samples = nb_frames;
 
@@ -351,7 +404,7 @@ int MediaEncoder::encode_audio(const AudioBuffer &buffer)
         sent_samples += frame->nb_samples;
 
         const auto buffer_size = \
-            av_samples_get_buffer_size(NULL, buffer.channels(),
+            av_samples_get_buffer_size(nullptr, buffer.channels(),
                                        frame->nb_samples, AV_SAMPLE_FMT_S16, 0);
 
         int err = avcodec_fill_audio_frame(frame, buffer.channels(),
@@ -368,7 +421,7 @@ int MediaEncoder::encode_audio(const AudioBuffer &buffer)
         nb_frames -= frame->nb_samples;
         offset_ptr += frame->nb_samples * buffer.channels();
 
-        encode(frame, stream_->index);
+        encode(frame, currentStreamIdx_);
         av_frame_free(&frame);
     }
 
@@ -379,33 +432,33 @@ int
 MediaEncoder::encode(AVFrame* frame, int streamIdx)
 {
     int ret = 0;
+    AVCodecContext* encoderCtx = encoders_[streamIdx];
     AVPacket pkt;
     av_init_packet(&pkt);
-    pkt.data = NULL; // packet data will be allocated by the encoder
+    pkt.data = nullptr; // packet data will be allocated by the encoder
     pkt.size = 0;
-    pkt.stream_index = streamIdx;
 
-    ret = avcodec_send_frame(encoderCtx_, frame);
+    ret = avcodec_send_frame(encoderCtx, frame);
     if (ret < 0)
         return -1;
 
     while (ret >= 0) {
-        ret = avcodec_receive_packet(encoderCtx_, &pkt);
+        ret = avcodec_receive_packet(encoderCtx, &pkt);
         if (ret == AVERROR(EAGAIN))
             break;
-        if (ret < 0) {
+        if (ret < 0 && ret != AVERROR_EOF) { // we still want to write our frame on EOF
             RING_ERR() << "Failed to encode frame: " << libav_utils::getError(ret);
             return ret;
         }
 
         if (pkt.size) {
+            pkt.stream_index = streamIdx;
             if (pkt.pts != AV_NOPTS_VALUE)
-                pkt.pts = av_rescale_q(pkt.pts, encoderCtx_->time_base,
-                                       stream_->time_base);
+                pkt.pts = av_rescale_q(pkt.pts, encoderCtx->time_base,
+                                       outputCtx_->streams[streamIdx]->time_base);
             if (pkt.dts != AV_NOPTS_VALUE)
-                pkt.dts = av_rescale_q(pkt.dts, encoderCtx_->time_base,
-                                       stream_->time_base);
-
+                pkt.dts = av_rescale_q(pkt.dts, encoderCtx->time_base,
+                                       outputCtx_->streams[streamIdx]->time_base);
 
             // write the compressed frame
             ret = av_write_frame(outputCtx_, &pkt);
@@ -423,7 +476,14 @@ MediaEncoder::encode(AVFrame* frame, int streamIdx)
 int
 MediaEncoder::flush()
 {
-    return encode(nullptr, stream_->index);
+    int ret = 0;
+    for (size_t i = 0; i < outputCtx_->nb_streams; ++i) {
+        if (encode(nullptr, i) < 0) {
+            RING_ERR() << "Could not flush stream #" << i;
+            ret |= 1u << i; // provide a way for caller to know which streams failed
+        }
+    }
+    return -ret;
 }
 
 std::string
@@ -431,9 +491,9 @@ MediaEncoder::print_sdp()
 {
     /* theora sdp can be huge */
 #ifndef _WIN32
-    const auto sdp_size = outputCtx_->streams[0]->codecpar->extradata_size + 2048;
+    const auto sdp_size = outputCtx_->streams[currentStreamIdx_]->codecpar->extradata_size + 2048;
 #else
-    const auto sdp_size = outputCtx_->streams[0]->codec->extradata_size + 2048;
+    const auto sdp_size = outputCtx_->streams[currentStreamIdx_]->codec->extradata_size + 2048;
 #endif
     std::string result;
     std::string sdp(sdp_size, '\0');
@@ -451,84 +511,102 @@ MediaEncoder::print_sdp()
     return result;
 }
 
-void MediaEncoder::prepareEncoderContext(bool is_video)
+AVCodecContext* MediaEncoder::prepareEncoderContext(AVCodec* outputCodec, bool is_video)
 {
-    encoderCtx_ = avcodec_alloc_context3(outputEncoder_);
+    AVCodecContext* encoderCtx = avcodec_alloc_context3(outputCodec);
 
-    auto encoderName = encoderCtx_->av_class->item_name ?
-        encoderCtx_->av_class->item_name(encoderCtx_) : nullptr;
+    auto encoderName = encoderCtx->av_class->item_name ?
+        encoderCtx->av_class->item_name(encoderCtx) : nullptr;
     if (encoderName == nullptr)
         encoderName = "encoder?";
 
-
-    encoderCtx_->thread_count = std::min(std::thread::hardware_concurrency(), is_video ? 16u : 4u);
-    RING_DBG("[%s] Using %d threads", encoderName, encoderCtx_->thread_count);
-
+    encoderCtx->thread_count = std::min(std::thread::hardware_concurrency(), is_video ? 16u : 4u);
+    RING_DBG("[%s] Using %d threads", encoderName, encoderCtx->thread_count);
 
     if (is_video) {
         // resolution must be a multiple of two
-        encoderCtx_->width = device_.width;
-        encoderCtx_->height = device_.height;
+        if (device_.width && device_.height) {
+            encoderCtx->width = device_.width;
+            encoderCtx->height = device_.height;
+        } else {
+            encoderCtx->width = atoi(av_dict_get(options_, "width", nullptr, 0)->value);
+            encoderCtx->height = atoi(av_dict_get(options_, "height", nullptr, 0)->value);
+        }
 
         // satisfy ffmpeg: denominator must be 16bit or less value
         // time base = 1/FPS
-        av_reduce(&encoderCtx_->time_base.den, &encoderCtx_->time_base.num,
-                  device_.framerate.numerator(), device_.framerate.denominator(),
-                  (1U << 16) - 1);
+        if (device_.framerate) {
+            av_dict_set(&options_, "width", ring::to_string(device_.width).c_str(), 0);
+            av_reduce(&encoderCtx->time_base.den, &encoderCtx->time_base.num,
+                      device_.framerate.numerator(), device_.framerate.denominator(),
+                      (1U << 16) - 1);
+        } else {
+            // get from options_, else default to 30 fps
+            auto v = av_dict_get(options_, "framerate", nullptr, 0);
+            AVRational framerate = AVRational{30, 1};
+            if (v)
+                av_parse_ratio_quiet(&framerate, v->value, 120);
+            if (framerate.den == 0)
+                framerate.den = 1;
+            av_reduce(&encoderCtx->time_base.den, &encoderCtx->time_base.num,
+                      framerate.num, framerate.den,
+                      (1U << 16) - 1);
+        }
 
         // emit one intra frame every gop_size frames
-        encoderCtx_->max_b_frames = 0;
-        encoderCtx_->pix_fmt = AV_PIX_FMT_YUV420P; // TODO: option me !
+        encoderCtx->max_b_frames = 0;
+        encoderCtx->pix_fmt = AV_PIX_FMT_YUV420P; // TODO: option me !
 
         // Fri Jul 22 11:37:59 EDT 2011:tmatth:XXX: DON'T set this, we want our
         // pps and sps to be sent in-band for RTP
         // This is to place global headers in extradata instead of every
         // keyframe.
-        // encoderCtx_->flags |= CODEC_FLAG_GLOBAL_HEADER;
-
+        // encoderCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
     } else {
-        encoderCtx_->sample_fmt = AV_SAMPLE_FMT_S16;
-        auto v = av_dict_get(options_, "sample_rate", NULL, 0);
+        encoderCtx->sample_fmt = AV_SAMPLE_FMT_S16;
+        auto v = av_dict_get(options_, "sample_rate", nullptr, 0);
         if (v) {
-            encoderCtx_->sample_rate = atoi(v->value);
-            encoderCtx_->time_base = AVRational{1, encoderCtx_->sample_rate};
+            encoderCtx->sample_rate = atoi(v->value);
+            encoderCtx->time_base = AVRational{1, encoderCtx->sample_rate};
         } else {
             RING_WARN("[%s] No sample rate set", encoderName);
-            encoderCtx_->sample_rate = 8000;
+            encoderCtx->sample_rate = 8000;
         }
 
-        v = av_dict_get(options_, "channels", NULL, 0);
+        v = av_dict_get(options_, "channels", nullptr, 0);
         if (v) {
             auto c = std::atoi(v->value);
             if (c > 2 or c < 1) {
                 RING_WARN("[%s] Clamping invalid channel value %d", encoderName, c);
                 c = 1;
             }
-            encoderCtx_->channels = c;
+            encoderCtx->channels = c;
         } else {
             RING_WARN("[%s] Channels not set", encoderName);
-            encoderCtx_->channels = 1;
+            encoderCtx->channels = 1;
         }
 
-        encoderCtx_->channel_layout = encoderCtx_->channels == 2 ? AV_CH_LAYOUT_STEREO : AV_CH_LAYOUT_MONO;
+        encoderCtx->channel_layout = encoderCtx->channels == 2 ? AV_CH_LAYOUT_STEREO : AV_CH_LAYOUT_MONO;
 
-        v = av_dict_get(options_, "frame_size", NULL, 0);
+        v = av_dict_get(options_, "frame_size", nullptr, 0);
         if (v) {
-            encoderCtx_->frame_size = atoi(v->value);
-            RING_DBG("[%s] Frame size %d", encoderName, encoderCtx_->frame_size);
+            encoderCtx->frame_size = atoi(v->value);
+            RING_DBG("[%s] Frame size %d", encoderName, encoderCtx->frame_size);
         } else {
             RING_WARN("[%s] Frame size not set", encoderName);
         }
     }
+
+    return encoderCtx;
 }
 
-void MediaEncoder::forcePresetX264()
+void MediaEncoder::forcePresetX264(AVCodecContext* encoderCtx)
 {
     const char *speedPreset = "ultrafast";
-    if (av_opt_set(encoderCtx_->priv_data, "preset", speedPreset, 0))
+    if (av_opt_set(encoderCtx->priv_data, "preset", speedPreset, 0))
         RING_WARN("Failed to set x264 preset '%s'", speedPreset);
     const char *tune = "zerolatency";
-    if (av_opt_set(encoderCtx_->priv_data, "tune", tune, 0))
+    if (av_opt_set(encoderCtx->priv_data, "tune", tune, 0))
         RING_WARN("Failed to set x264 tune '%s'", tune);
 }
 
@@ -592,4 +670,13 @@ MediaEncoder::useCodec(const ring::AccountCodecInfo* codec) const noexcept
     return codec_.get() == codec;
 }
 
+unsigned
+MediaEncoder::getStreamCount() const
+{
+    if (outputCtx_)
+        return outputCtx_->nb_streams;
+    else
+        return 0;
+}
+
 } // namespace ring
diff --git a/src/media/media_encoder.h b/src/media/media_encoder.h
index 44e824411844bea3c630ffb262b5819c48bffab8..fd95af5e36c3fac1b4c14c203197b085d1f7ad36 100644
--- a/src/media/media_encoder.h
+++ b/src/media/media_encoder.h
@@ -30,6 +30,7 @@
 
 #include "noncopyable.h"
 #include "media_buffer.h"
+#include "media_codec.h"
 #include "media_device.h"
 
 #include <map>
@@ -63,7 +64,9 @@ public:
     void setInterruptCallback(int (*cb)(void*), void *opaque);
 
     void setDeviceOptions(const DeviceParams& args);
-    void openOutput(const std::string& filename, const MediaDescription& args);
+    void openLiveOutput(const std::string& filename, const MediaDescription& args);
+    void openFileOutput(const std::string& filename, std::map<std::string, std::string> options);
+    int addStream(const SystemCodecInfo& codec, std::string parameters = "");
     void startIO();
     void setIOContext(const std::unique_ptr<MediaIOHandle> &ioctx);
 
@@ -80,7 +83,7 @@ public:
     std::string print_sdp();
 
     /* getWidth and getHeight return size of the encoded frame.
-     * Values have meaning only after openOutput call.
+     * Values have meaning only after openLiveOutput call.
      */
     int getWidth() const { return device_.width; }
     int getHeight() const { return device_.height; }
@@ -92,18 +95,19 @@ public:
 
     bool useCodec(const AccountCodecInfo* codec) const noexcept;
 
+    unsigned getStreamCount() const;
+
 private:
     NON_COPYABLE(MediaEncoder);
     void setOptions(const MediaDescription& args);
     void setScaleDest(void *data, int width, int height, int pix_fmt);
-    void prepareEncoderContext(bool is_video);
-    void forcePresetX264();
+    AVCodecContext* prepareEncoderContext(AVCodec* outputCodec, bool is_video);
+    void forcePresetX264(AVCodecContext* encoderCtx);
     void extractProfileLevelID(const std::string &parameters, AVCodecContext *ctx);
 
-    AVCodec *outputEncoder_ = nullptr;
-    AVCodecContext *encoderCtx_ = nullptr;
+    std::vector<AVCodecContext*> encoders_;
     AVFormatContext *outputCtx_ = nullptr;
-    AVStream *stream_ = nullptr;
+    int currentStreamIdx_ = -1;
     unsigned sent_samples = 0;
 
 #ifdef RING_VIDEO
@@ -113,7 +117,6 @@ private:
 
     std::vector<uint8_t> scaledFrameBuffer_;
     int scaledFrameBufferSize_ = 0;
-    int streamIndex_ = -1;
     bool is_muted = false;
 
 protected:
diff --git a/src/media/video/video_sender.cpp b/src/media/video/video_sender.cpp
index a0c6ba2c29d7037396c1d992da296c13d62ad3de..8fb9916e0e68b682bcc65aa8adf90727f99faa55 100644
--- a/src/media/video/video_sender.cpp
+++ b/src/media/video/video_sender.cpp
@@ -44,7 +44,7 @@ VideoSender::VideoSender(const std::string& dest, const DeviceParams& dev,
 {
     videoEncoder_->setDeviceOptions(dev);
     keyFrameFreq_ = dev.framerate.numerator() * KEY_FRAME_PERIOD;
-    videoEncoder_->openOutput(dest, args);
+    videoEncoder_->openLiveOutput(dest, args);
     videoEncoder_->setInitSeqVal(seqVal);
     videoEncoder_->setIOContext(muxContext_);
     videoEncoder_->startIO();
diff --git a/test/unitTest/Makefile.am b/test/unitTest/Makefile.am
index 63606c3c63d4d7bc1b9455a538991880f2a43cd8..7e71fa76781e871a1f8fb4db3b5a79fa32478f93 100644
--- a/test/unitTest/Makefile.am
+++ b/test/unitTest/Makefile.am
@@ -61,4 +61,10 @@ ut_string_utils_SOURCES = string_utils/testString_utils.cpp
 check_PROGRAMS += ut_video_input
 ut_video_input_SOURCES = media/video/testVideo_input.cpp
 
+#
+# media_encoder
+#
+check_PROGRAMS += ut_media_encoder
+ut_media_encoder_SOURCES = media/test_media_encoder.cpp
+
 TESTS = $(check_PROGRAMS)
diff --git a/test/unitTest/media/test_media_encoder.cpp b/test/unitTest/media/test_media_encoder.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..2a1fb24eb539355bf074dfb1336fee91fbd2320a
--- /dev/null
+++ b/test/unitTest/media/test_media_encoder.cpp
@@ -0,0 +1,202 @@
+/*
+ *  Copyright (C) 2018 Savoir-faire Linux Inc.
+ *
+ *  Author: Philippe Gorley <philippe.gorley@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#include <cppunit/TestAssert.h>
+#include <cppunit/TestFixture.h>
+#include <cppunit/extensions/HelperMacros.h>
+
+#include "dring.h"
+#include "fileutils.h"
+#include "libav_deps.h"
+#include "media_encoder.h"
+#include "media_io_handle.h"
+#include "system_codec_container.h"
+
+#include "../../test_runner.h"
+
+namespace ring { namespace test {
+
+class MediaEncoderTest : public CppUnit::TestFixture {
+public:
+    static std::string name() { return "media_encoder"; }
+
+    void setUp();
+    void tearDown();
+
+private:
+    void testMultiStream();
+
+    CPPUNIT_TEST_SUITE(MediaEncoderTest);
+    CPPUNIT_TEST(testMultiStream);
+    CPPUNIT_TEST_SUITE_END();
+
+    std::unique_ptr<MediaEncoder> encoder_;
+    std::unique_ptr<MediaIOHandle> ioHandle_;
+    std::vector<std::string> files_;
+    std::string cacheDir_;
+};
+
+CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(MediaEncoderTest, MediaEncoderTest::name());
+
+void
+MediaEncoderTest::setUp()
+{
+    DRing::init(DRing::InitFlag(DRing::DRING_FLAG_DEBUG | DRing::DRING_FLAG_CONSOLE_LOG));
+    libav_utils::ring_avcodec_init();
+    encoder_.reset(new MediaEncoder);
+    cacheDir_ = fileutils::get_cache_dir() + DIR_SEPARATOR_CH;
+    files_.push_back(cacheDir_ + "test.mkv");
+}
+
+void
+MediaEncoderTest::tearDown()
+{
+    // clean up behind ourselves
+    for (const auto& file : files_)
+        fileutils::remove(cacheDir_ + file);
+    DRing::fini();
+}
+
+static AVFrame*
+getVideoFrame(int width, int height, int frame_index)
+{
+    int x, y;
+    AVFrame* frame = av_frame_alloc();
+    if (!frame)
+        return nullptr;
+
+    frame->format = AV_PIX_FMT_YUV420P;
+    frame->width = width;
+    frame->height = height;
+
+    if (av_frame_get_buffer(frame, 32) < 0) {
+        av_frame_free(&frame);
+        return nullptr;
+    }
+
+    /* Y */
+    for (y = 0; y < height; y++)
+        for (x = 0; x < width; x++)
+            frame->data[0][y * frame->linesize[0] + x] = x + y + frame_index * 3;
+
+    /* Cb and Cr */
+    for (y = 0; y < height / 2; y++) {
+        for (x = 0; x < width / 2; x++) {
+            frame->data[1][y * frame->linesize[1] + x] = 128 + y + frame_index * 2;
+            frame->data[2][y * frame->linesize[2] + x] = 64 + x + frame_index * 5;
+        }
+    }
+
+    return frame;
+}
+
+static AVFrame*
+getAudioFrame(int sampleRate, int nbSamples, int nbChannels)
+{
+    const constexpr float pi = 3.14159265358979323846264338327950288; // M_PI
+    const float tincr = 2 * pi * 440.0 / sampleRate;
+    float t = 0;
+    AVFrame* frame = av_frame_alloc();
+    if (!frame)
+        return nullptr;
+
+    frame->format = AV_SAMPLE_FMT_S16;
+    frame->channels = nbChannels;
+    frame->channel_layout = av_get_default_channel_layout(nbChannels);
+    frame->nb_samples = nbSamples;
+    frame->sample_rate = sampleRate;
+
+    if (av_frame_get_buffer(frame, 0) < 0) {
+        av_frame_free(&frame);
+        return nullptr;
+    }
+
+    auto samples = reinterpret_cast<uint16_t*>(frame->data[0]);
+    for (int i = 0; i < 200; ++i) {
+        for (int j = 0; j < nbSamples; ++j) {
+            samples[2 * j] = static_cast<int>(sin(t) * 10000);
+            for (int k = 1; k < nbChannels; ++k) {
+                samples[2 * j + k] = samples[2 * j];
+            }
+            t += tincr;
+        }
+    }
+
+    return frame;
+}
+
+void
+MediaEncoderTest::testMultiStream()
+{
+    const constexpr int sampleRate = 48000;
+    const constexpr int nbChannels = 2;
+    const constexpr int width = 320;
+    const constexpr int height = 240;
+    std::map<std::string, std::string> options;
+    options["sample_rate"] = std::to_string(sampleRate);
+    options["channels"] = std::to_string(nbChannels);
+    options["width"] = std::to_string(width);
+    options["height"] = std::to_string(height);
+    auto vp8Codec = std::static_pointer_cast<ring::SystemVideoCodecInfo>(
+        getSystemCodecContainer()->searchCodecByName("VP8", ring::MEDIA_VIDEO)
+    );
+    auto opusCodec = std::static_pointer_cast<SystemAudioCodecInfo>(
+        getSystemCodecContainer()->searchCodecByName("opus", ring::MEDIA_AUDIO)
+    );
+
+    try {
+        encoder_->openFileOutput(cacheDir_ + "test.mkv", options);
+        encoder_->setIOContext(ioHandle_);
+        int videoIdx = encoder_->addStream(*vp8Codec.get());
+        CPPUNIT_ASSERT(videoIdx >= 0);
+        CPPUNIT_ASSERT(encoder_->getStreamCount() == 1);
+        int audioIdx = encoder_->addStream(*opusCodec.get());
+        CPPUNIT_ASSERT(audioIdx >= 0);
+        CPPUNIT_ASSERT(videoIdx != audioIdx);
+        CPPUNIT_ASSERT(encoder_->getStreamCount() == 2);
+        encoder_->startIO();
+
+        int sentSamples = 0;
+        AVFrame* audio = nullptr;
+        AVFrame* video = nullptr;
+        for (int i = 0; i < 25; ++i) {
+            audio = getAudioFrame(sampleRate, 0.02*sampleRate, nbChannels);
+            CPPUNIT_ASSERT(audio);
+            audio->pts = sentSamples;
+            video = getVideoFrame(width, height, i);
+            CPPUNIT_ASSERT(video);
+            video->pts = i;
+
+            CPPUNIT_ASSERT(encoder_->encode(audio, audioIdx) >= 0);
+            sentSamples += audio->nb_samples;
+            CPPUNIT_ASSERT(encoder_->encode(video, videoIdx) >= 0);
+
+            av_frame_free(&audio);
+            av_frame_free(&video);
+        }
+        CPPUNIT_ASSERT(encoder_->flush() >= 0);
+    } catch (const MediaEncoderException& e) {
+        CPPUNIT_FAIL(e.what());
+    }
+}
+
+}} // namespace ring::test
+
+RING_TEST_RUNNER(ring::test::MediaEncoderTest::name());