Do EncodeServerFinder 'disable' in a more sensible way.
[dcpomatic.git] / src / lib / encoder.cc
index 0ac32d3bfa9cd2eb099360080d9ac6e4657d715d..0c41d8d806a8ec9bf627a54b2ac579315edab074 100644 (file)
@@ -1,19 +1,20 @@
 /*
-    Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2015 Carl Hetherington <cth@carlh.net>
 
-    This program is free software; you can redistribute it and/or modify
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic 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 2 of the License, or
     (at your option) any later version.
 
-    This program is distributed in the hope that it will be useful,
+    DCP-o-matic 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
 
 */
 
  *  @brief Parent class for classes which can encode video and audio frames.
  */
 
-#include <iostream>
-#include <boost/filesystem.hpp>
-#include <boost/lexical_cast.hpp>
-#include <libdcp/picture_asset.h>
 #include "encoder.h"
 #include "util.h"
-#include "options.h"
 #include "film.h"
 #include "log.h"
-#include "exceptions.h"
-#include "filter.h"
 #include "config.h"
-#include "dcp_video_frame.h"
-#include "server.h"
-#include "format.h"
+#include "dcp_video.h"
 #include "cross.h"
 #include "writer.h"
+#include "encode_server_finder.h"
+#include "player.h"
+#include "player_video.h"
+#include "encode_server_description.h"
+#include "compose.hpp"
+#include <libcxml/cxml.h>
+#include <boost/foreach.hpp>
+#include <iostream>
 
 #include "i18n.h"
 
-using std::pair;
-using std::string;
-using std::stringstream;
-using std::vector;
+#define LOG_GENERAL(...) _film->log()->log (String::compose (__VA_ARGS__), LogEntry::TYPE_GENERAL);
+#define LOG_GENERAL_NC(...) _film->log()->log (__VA_ARGS__, LogEntry::TYPE_GENERAL);
+#define LOG_ERROR(...) _film->log()->log (String::compose (__VA_ARGS__), LogEntry::TYPE_ERROR);
+#define LOG_TIMING(...) _film->log()->log (String::compose (__VA_ARGS__), LogEntry::TYPE_TIMING);
+
 using std::list;
 using std::cout;
-using std::min;
-using std::make_pair;
-using namespace boost;
+using boost::shared_ptr;
+using boost::weak_ptr;
+using boost::optional;
+using dcp::Data;
 
-int const Encoder::_history_size = 25;
+int const Encoder::_history_size = 200;
 
 /** @param f Film that we are encoding */
-Encoder::Encoder (shared_ptr<Film> f)
-       : _film (f)
-       , _video_frames_in (0)
-       , _video_frames_out (0)
-#ifdef HAVE_SWRESAMPLE   
-       , _swr_context (0)
-#endif
-       , _have_a_real_frame (false)
-       , _terminate (false)
+Encoder::Encoder (shared_ptr<const Film> film, shared_ptr<Writer> writer)
+       : _film (film)
+       , _writer (writer)
 {
-       
+       servers_list_changed ();
 }
 
 Encoder::~Encoder ()
 {
-       terminate_threads ();
-       if (_writer) {
-               _writer->finish ();
+       try {
+               terminate_threads ();
+       } catch (...) {
+               /* Destructors must not throw exceptions; anything bad
+                  happening now is too late to worry about anyway,
+                  I think.
+               */
        }
 }
 
 void
-Encoder::process_begin ()
+Encoder::begin ()
 {
-       if (_film->audio_stream() && _film->audio_stream()->sample_rate() != _film->target_audio_sample_rate()) {
-#ifdef HAVE_SWRESAMPLE
-
-               stringstream s;
-               s << String::compose (N_("Will resample audio from %1 to %2"), _film->audio_stream()->sample_rate(), _film->target_audio_sample_rate());
-               _film->log()->log (s.str ());
-
-               /* We will be using planar float data when we call the resampler */
-               _swr_context = swr_alloc_set_opts (
-                       0,
-                       _film->audio_stream()->channel_layout(),
-                       AV_SAMPLE_FMT_FLTP,
-                       _film->target_audio_sample_rate(),
-                       _film->audio_stream()->channel_layout(),
-                       AV_SAMPLE_FMT_FLTP,
-                       _film->audio_stream()->sample_rate(),
-                       0, 0
-                       );
-               
-               swr_init (_swr_context);
-#else
-               throw EncodeError (_("Cannot resample audio as libswresample is not present"));
-#endif
-       } else {
-#ifdef HAVE_SWRESAMPLE
-               _swr_context = 0;
-#endif         
-       }
-
-       for (int i = 0; i < Config::instance()->num_local_encoding_threads (); ++i) {
-               _threads.push_back (new boost::thread (boost::bind (&Encoder::encoder_thread, this, (ServerDescription *) 0)));
-       }
-
-       vector<ServerDescription*> servers = Config::instance()->servers ();
-
-       for (vector<ServerDescription*>::iterator i = servers.begin(); i != servers.end(); ++i) {
-               for (int j = 0; j < (*i)->threads (); ++j) {
-                       _threads.push_back (new boost::thread (boost::bind (&Encoder::encoder_thread, this, *i)));
-               }
-       }
-
-       _writer.reset (new Writer (_film));
+       weak_ptr<Encoder> wp = shared_from_this ();
+       _server_found_connection = EncodeServerFinder::instance()->ServersListChanged.connect (
+               boost::bind (&Encoder::call_servers_list_changed, wp)
+               );
 }
 
-
+/* We don't want the servers-list-changed callback trying to do things
+   during destruction of Encoder, and I think this is the neatest way
+   to achieve that.
+*/
 void
-Encoder::process_end ()
+Encoder::call_servers_list_changed (weak_ptr<Encoder> encoder)
 {
-#if HAVE_SWRESAMPLE    
-       if (_film->audio_stream() && _film->audio_stream()->channels() && _swr_context) {
-
-               shared_ptr<AudioBuffers> out (new AudioBuffers (_film->audio_stream()->channels(), 256));
-                       
-               while (1) {
-                       int const frames = swr_convert (_swr_context, (uint8_t **) out->data(), 256, 0, 0);
-
-                       if (frames < 0) {
-                               throw EncodeError (_("could not run sample-rate converter"));
-                       }
-
-                       if (frames == 0) {
-                               break;
-                       }
-
-                       out->set_frames (frames);
-                       write_audio (out);
-               }
-
-               swr_free (&_swr_context);
-       }
-#endif
-
-       if (_film->audio_channels() == 0 && _film->minimum_audio_channels() > 0) {
-               /* Put audio in where there is none at all */
-               int64_t af = video_frames_to_audio_frames (_video_frames_out, 48000, _film->dcp_frame_rate ());
-               while (af) {
-                       int64_t const this_time = min (af, static_cast<int64_t> (24000));
-                       shared_ptr<AudioBuffers> out (new AudioBuffers (_film->minimum_audio_channels(), this_time));
-                       out->make_silent ();
-                       out->set_frames (this_time);
-                       write_audio (out);
-
-                       af -= this_time;
-               }
+       shared_ptr<Encoder> e = encoder.lock ();
+       if (e) {
+               e->servers_list_changed ();
        }
+}
 
-       boost::mutex::scoped_lock lock (_mutex);
+void
+Encoder::end ()
+{
+       boost::mutex::scoped_lock lock (_queue_mutex);
 
-       _film->log()->log (String::compose (N_("Clearing queue of %1"), _queue.size ()));
+       LOG_GENERAL (N_("Clearing queue of %1"), _queue.size ());
 
        /* Keep waking workers until the queue is empty */
        while (!_queue.empty ()) {
-               _film->log()->log (String::compose (N_("Waking with %1"), _queue.size ()), Log::VERBOSE);
-               _condition.notify_all ();
-               _condition.wait (lock);
+               rethrow ();
+               _empty_condition.notify_all ();
+               _full_condition.wait (lock);
        }
 
        lock.unlock ();
-       
+
+       LOG_GENERAL_NC (N_("Terminating encoder threads"));
+
        terminate_threads ();
 
-       _film->log()->log (String::compose (N_("Mopping up %1"), _queue.size()));
+       LOG_GENERAL (N_("Mopping up %1"), _queue.size());
 
        /* The following sequence of events can occur in the above code:
             1. a remote worker takes the last image off the queue
@@ -190,27 +128,28 @@ Encoder::process_end ()
             So just mop up anything left in the queue here.
        */
 
-       for (list<shared_ptr<DCPVideoFrame> >::iterator i = _queue.begin(); i != _queue.end(); ++i) {
-               _film->log()->log (String::compose (N_("Encode left-over frame %1"), (*i)->frame ()));
+       for (list<shared_ptr<DCPVideo> >::iterator i = _queue.begin(); i != _queue.end(); ++i) {
+               LOG_GENERAL (N_("Encode left-over frame %1"), (*i)->index ());
                try {
-                       _writer->write ((*i)->encode_locally(), (*i)->frame ());
+                       _writer->write (
+                               (*i)->encode_locally (boost::bind (&Log::dcp_log, _film->log().get(), _1, _2)),
+                               (*i)->index (),
+                               (*i)->eyes ()
+                               );
                        frame_done ();
                } catch (std::exception& e) {
-                       _film->log()->log (String::compose (N_("Local encode failed (%1)"), e.what ()));
+                       LOG_ERROR (N_("Local encode failed (%1)"), e.what ());
                }
        }
-
-       _writer->finish ();
-       _writer.reset ();
-}      
+}
 
 /** @return an estimate of the current number of frames we are encoding per second,
  *  or 0 if not known.
  */
 float
-Encoder::current_frames_per_second () const
+Encoder::current_encoding_rate () const
 {
-       boost::mutex::scoped_lock lock (_history_mutex);
+       boost::mutex::scoped_lock lock (_state_mutex);
        if (int (_time_history.size()) < _history_size) {
                return 0;
        }
@@ -221,12 +160,15 @@ Encoder::current_frames_per_second () const
        return _history_size / (seconds (now) - seconds (_time_history.back ()));
 }
 
-/** @return Number of video frames that have been sent out */
+/** @return Number of video frames that have been queued for encoding */
 int
-Encoder::video_frames_out () const
+Encoder::video_frames_enqueued () const
 {
-       boost::mutex::scoped_lock (_history_mutex);
-       return _video_frames_out;
+       if (!_last_player_video) {
+               return 0;
+       }
+
+       return _last_player_video->time().frames_floor (_film->video_frame_rate ());
 }
 
 /** Should be called when a frame has been encoded successfully.
@@ -235,8 +177,8 @@ Encoder::video_frames_out () const
 void
 Encoder::frame_done ()
 {
-       boost::mutex::scoped_lock lock (_history_mutex);
-       
+       boost::mutex::scoped_lock lock (_state_mutex);
+
        struct timeval tv;
        gettimeofday (&tv, 0);
        _time_history.push_front (tv);
@@ -245,226 +187,231 @@ Encoder::frame_done ()
        }
 }
 
+/** Called to start encoding of the next video frame in the DCP.  This is called in order,
+ *  so each time the supplied frame is the one after the previous one.
+ *  pv represents one video frame, and could be empty if there is nothing to encode
+ *  for this DCP frame.
+ */
 void
-Encoder::process_video (shared_ptr<const Image> image, bool same, boost::shared_ptr<Subtitle> sub)
+Encoder::encode (shared_ptr<PlayerVideo> pv)
 {
-       FrameRateConversion frc (_film->source_frame_rate(), _film->dcp_frame_rate());
-       
-       if (frc.skip && (_video_frames_in % 2)) {
-               ++_video_frames_in;
-               return;
+       _waker.nudge ();
+
+       size_t threads = 0;
+       {
+               boost::mutex::scoped_lock threads_lock (_threads_mutex);
+               threads = _threads.size ();
        }
 
-       boost::mutex::scoped_lock lock (_mutex);
+       boost::mutex::scoped_lock queue_lock (_queue_mutex);
 
-       /* Wait until the queue has gone down a bit */
-       while (_queue.size() >= _threads.size() * 2 && !_terminate) {
-               TIMING ("decoder sleeps with queue of %1", _queue.size());
-               _condition.wait (lock);
-               TIMING ("decoder wakes with queue of %1", _queue.size());
+       /* Wait until the queue has gone down a bit.  Allow one thing in the queue even
+          when there are no threads.
+       */
+       while (_queue.size() >= (threads * 2) + 1) {
+               LOG_TIMING ("decoder-sleep queue=%1 threads=%2", _queue.size(), threads);
+               _full_condition.wait (queue_lock);
+               LOG_TIMING ("decoder-wake queue=%1 threads=%2", _queue.size(), threads);
        }
 
-       if (_terminate) {
-               return;
-       }
+       _writer->rethrow ();
+       /* Re-throw any exception raised by one of our threads.  If more
+          than one has thrown an exception, only one will be rethrown, I think;
+          but then, if that happens something has gone badly wrong.
+       */
+       rethrow ();
 
-       if (_writer->thrown ()) {
-               _writer->rethrow ();
-       }
+       Frame const position = pv->time().frames_floor(_film->video_frame_rate());
 
-       if (_writer->can_fake_write (_video_frames_out)) {
-               _writer->fake_write (_video_frames_out);
-               _have_a_real_frame = false;
-               frame_done ();
-       } else if (same && _have_a_real_frame) {
-               /* Use the last frame that we encoded. */
-               _writer->repeat (_video_frames_out);
+       if (_writer->can_fake_write (position)) {
+               /* We can fake-write this frame */
+               _writer->fake_write (position, pv->eyes ());
                frame_done ();
+       } else if (pv->has_j2k ()) {
+               /* This frame already has JPEG2000 data, so just write it */
+               _writer->write (pv->j2k(), position, pv->eyes ());
+       } else if (_last_player_video && _writer->can_repeat(position) && pv->same (_last_player_video)) {
+               _writer->repeat (position, pv->eyes ());
        } else {
                /* Queue this new frame for encoding */
-               pair<string, string> const s = Filter::ffmpeg_strings (_film->filters());
-               TIMING ("adding to queue of %1", _queue.size ());
-               _queue.push_back (boost::shared_ptr<DCPVideoFrame> (
-                                         new DCPVideoFrame (
-                                                 image, sub, _film->format()->dcp_size(), _film->format()->dcp_padding (_film),
-                                                 _film->subtitle_offset(), _film->subtitle_scale(),
-                                                 _film->scaler(), _video_frames_out, _film->dcp_frame_rate(), s.second,
-                                                 _film->colour_lut(), _film->j2k_bandwidth(),
+               LOG_TIMING ("add-frame-to-queue queue=%1", _queue.size ());
+               _queue.push_back (shared_ptr<DCPVideo> (
+                                         new DCPVideo (
+                                                 pv,
+                                                 position,
+                                                 _film->video_frame_rate(),
+                                                 _film->j2k_bandwidth(),
+                                                 _film->resolution(),
                                                  _film->log()
                                                  )
                                          ));
-               
-               _condition.notify_all ();
-               _have_a_real_frame = true;
-       }
-
-       ++_video_frames_in;
-       ++_video_frames_out;
 
-       if (frc.repeat) {
-               _writer->repeat (_video_frames_out);
-               ++_video_frames_out;
-               frame_done ();
+               /* The queue might not be empty any more, so notify anything which is
+                  waiting on that.
+               */
+               _empty_condition.notify_all ();
        }
-}
 
-void
-Encoder::process_audio (shared_ptr<const AudioBuffers> data)
-{
-       if (!data->frames ()) {
-               return;
-       }
-       
-#if HAVE_SWRESAMPLE
-       /* Maybe sample-rate convert */
-       if (_swr_context) {
-
-               /* Compute the resampled frames count and add 32 for luck */
-               int const max_resampled_frames = ceil ((int64_t) data->frames() * _film->target_audio_sample_rate() / _film->audio_stream()->sample_rate()) + 32;
-
-               shared_ptr<AudioBuffers> resampled (new AudioBuffers (_film->audio_stream()->channels(), max_resampled_frames));
-
-               /* Resample audio */
-               int const resampled_frames = swr_convert (
-                       _swr_context, (uint8_t **) resampled->data(), max_resampled_frames, (uint8_t const **) data->data(), data->frames()
-                       );
-               
-               if (resampled_frames < 0) {
-                       throw EncodeError (_("could not run sample-rate converter"));
-               }
-
-               resampled->set_frames (resampled_frames);
-               
-               /* And point our variables at the resampled audio */
-               data = resampled;
-       }
-#endif
-
-       write_audio (data);
+       _last_player_video = pv;
 }
 
 void
 Encoder::terminate_threads ()
 {
-       boost::mutex::scoped_lock lock (_mutex);
-       _terminate = true;
-       _condition.notify_all ();
-       lock.unlock ();
+       boost::mutex::scoped_lock threads_lock (_threads_mutex);
 
+       int n = 0;
        for (list<boost::thread *>::iterator i = _threads.begin(); i != _threads.end(); ++i) {
-               if ((*i)->joinable ()) {
+               LOG_GENERAL ("Terminating thread %1 of %2", n + 1, _threads.size ());
+               (*i)->interrupt ();
+               DCPOMATIC_ASSERT ((*i)->joinable ());
+               try {
                        (*i)->join ();
+               } catch (boost::thread_interrupted& e) {
+                       /* This is to be expected */
                }
                delete *i;
+               LOG_GENERAL_NC ("Thread terminated");
+               ++n;
        }
 
        _threads.clear ();
 }
 
 void
-Encoder::encoder_thread (ServerDescription* server)
+Encoder::encoder_thread (optional<EncodeServerDescription> server)
+try
 {
+       if (server) {
+               LOG_TIMING ("start-encoder-thread thread=%1 server=%2", boost::this_thread::get_id (), server->host_name ());
+       } else {
+               LOG_TIMING ("start-encoder-thread thread=%1 server=localhost", boost::this_thread::get_id ());
+       }
+
        /* Number of seconds that we currently wait between attempts
           to connect to the server; not relevant for localhost
           encodings.
        */
        int remote_backoff = 0;
-       
-       while (1) {
 
-               TIMING ("encoder thread %1 sleeps", boost::this_thread::get_id());
-               boost::mutex::scoped_lock lock (_mutex);
-               while (_queue.empty () && !_terminate) {
-                       _condition.wait (lock);
-               }
+       while (true) {
 
-               if (_terminate) {
-                       return;
+               LOG_TIMING ("encoder-sleep thread=%1", boost::this_thread::get_id());
+               boost::mutex::scoped_lock lock (_queue_mutex);
+               while (_queue.empty ()) {
+                       _empty_condition.wait (lock);
                }
 
-               TIMING ("encoder thread %1 wakes with queue of %2", boost::this_thread::get_id(), _queue.size());
-               boost::shared_ptr<DCPVideoFrame> vf = _queue.front ();
-               _film->log()->log (String::compose (N_("Encoder thread %1 pops frame %2 from queue"), boost::this_thread::get_id(), vf->frame()), Log::VERBOSE);
+               LOG_TIMING ("encoder-wake thread=%1 queue=%2", boost::this_thread::get_id(), _queue.size());
+               shared_ptr<DCPVideo> vf = _queue.front ();
+               LOG_TIMING ("encoder-pop thread=%1 frame=%2 eyes=%3", boost::this_thread::get_id(), vf->index(), vf->eyes ());
                _queue.pop_front ();
-               
+
                lock.unlock ();
 
-               shared_ptr<EncodedData> encoded;
+               optional<Data> encoded;
 
+               /* We need to encode this input */
                if (server) {
                        try {
-                               encoded = vf->encode_remotely (server);
+                               encoded = vf->encode_remotely (server.get ());
 
                                if (remote_backoff > 0) {
-                                       _film->log()->log (String::compose (N_("%1 was lost, but now she is found; removing backoff"), server->host_name ()));
+                                       LOG_GENERAL ("%1 was lost, but now she is found; removing backoff", server->host_name ());
                                }
-                               
+
                                /* This job succeeded, so remove any backoff */
                                remote_backoff = 0;
-                               
+
                        } catch (std::exception& e) {
                                if (remote_backoff < 60) {
                                        /* back off more */
                                        remote_backoff += 10;
                                }
-                               _film->log()->log (
-                                       String::compose (
-                                               N_("Remote encode of %1 on %2 failed (%3); thread sleeping for %4s"),
-                                               vf->frame(), server->host_name(), e.what(), remote_backoff)
+                               LOG_ERROR (
+                                       N_("Remote encode of %1 on %2 failed (%3); thread sleeping for %4s"),
+                                       vf->index(), server->host_name(), e.what(), remote_backoff
                                        );
                        }
-                               
+
                } else {
                        try {
-                               TIMING ("encoder thread %1 begins local encode of %2", boost::this_thread::get_id(), vf->frame());
-                               encoded = vf->encode_locally ();
-                               TIMING ("encoder thread %1 finishes local encode of %2", boost::this_thread::get_id(), vf->frame());
+                               LOG_TIMING ("start-local-encode thread=%1 frame=%2", boost::this_thread::get_id(), vf->index());
+                               encoded = vf->encode_locally (boost::bind (&Log::dcp_log, _film->log().get(), _1, _2));
+                               LOG_TIMING ("finish-local-encode thread=%1 frame=%2", boost::this_thread::get_id(), vf->index());
                        } catch (std::exception& e) {
-                               _film->log()->log (String::compose (N_("Local encode failed (%1)"), e.what ()));
+                               LOG_ERROR (N_("Local encode failed (%1)"), e.what ());
+                               throw;
                        }
                }
 
                if (encoded) {
-                       _writer->write (encoded, vf->frame ());
+                       _writer->write (encoded.get(), vf->index (), vf->eyes ());
                        frame_done ();
                } else {
                        lock.lock ();
-                       _film->log()->log (
-                               String::compose (N_("Encoder thread %1 pushes frame %2 back onto queue after failure"), boost::this_thread::get_id(), vf->frame())
-                               );
+                       LOG_GENERAL (N_("[%1] Encoder thread pushes frame %2 back onto queue after failure"), boost::this_thread::get_id(), vf->index());
                        _queue.push_front (vf);
                        lock.unlock ();
                }
 
                if (remote_backoff > 0) {
-                       dvdomatic_sleep (remote_backoff);
+                       boost::this_thread::sleep (boost::posix_time::seconds (remote_backoff));
                }
 
+               /* The queue might not be full any more, so notify anything that is waiting on that */
                lock.lock ();
-               _condition.notify_all ();
+               _full_condition.notify_all ();
        }
 }
+catch (boost::thread_interrupted& e) {
+       /* Ignore these and just stop the thread */
+       _full_condition.notify_all ();
+}
+catch (...)
+{
+       store_current ();
+       /* Wake anything waiting on _full_condition so it can see the exception */
+       _full_condition.notify_all ();
+}
 
 void
-Encoder::write_audio (shared_ptr<const AudioBuffers> data)
+Encoder::servers_list_changed ()
 {
-       AudioMapping m (_film);
-       if (m.dcp_channels() != _film->audio_channels()) {
-
-               /* Remap and pad with silence */
-
-               shared_ptr<AudioBuffers> b (new AudioBuffers (m.dcp_channels(), data->frames ()));
-               for (int i = 0; i < m.dcp_channels(); ++i) {
-                       optional<int> s = m.dcp_to_source (static_cast<libdcp::Channel> (i));
-                       if (!s) {
-                               b->make_silent (i);
-                       } else {
-                               memcpy (b->data()[i], data->data()[s.get()], data->frames() * sizeof(float));
+       terminate_threads ();
+
+       /* XXX: could re-use threads */
+
+       boost::mutex::scoped_lock lm (_threads_mutex);
+
+#ifdef BOOST_THREAD_PLATFORM_WIN32
+       OSVERSIONINFO info;
+       info.dwOSVersionInfoSize = sizeof (OSVERSIONINFO);
+       GetVersionEx (&info);
+       bool const windows_xp = (info.dwMajorVersion == 5 && info.dwMinorVersion == 1);
+       if (windows_xp) {
+               LOG_GENERAL_NC (N_("Setting thread affinity for Windows XP"));
+       }
+#endif
+
+       if (!Config::instance()->only_servers_encode ()) {
+               for (int i = 0; i < Config::instance()->num_local_encoding_threads (); ++i) {
+                       boost::thread* t = new boost::thread (boost::bind (&Encoder::encoder_thread, this, optional<EncodeServerDescription> ()));
+                       _threads.push_back (t);
+#ifdef BOOST_THREAD_PLATFORM_WIN32
+                       if (windows_xp) {
+                               SetThreadAffinityMask (t->native_handle(), 1 << i);
                        }
+#endif
                }
+       }
 
-               data = b;
+       BOOST_FOREACH (EncodeServerDescription i, EncodeServerFinder::instance()->servers ()) {
+               LOG_GENERAL (N_("Adding %1 worker threads for remote %2"), i.threads(), i.host_name ());
+               for (int j = 0; j < i.threads(); ++j) {
+                       _threads.push_back (new boost::thread (boost::bind (&Encoder::encoder_thread, this, i)));
+               }
        }
 
-       _writer->write (data);
+       _writer->set_encoder_threads (_threads.size ());
 }