Merge master.
authorCarl Hetherington <cth@carlh.net>
Tue, 20 May 2014 12:23:26 +0000 (13:23 +0100)
committerCarl Hetherington <cth@carlh.net>
Tue, 20 May 2014 12:23:26 +0000 (13:23 +0100)
34 files changed:
ChangeLog
debian/changelog
src/lib/colour_conversion.cc
src/lib/content_video.h
src/lib/dcp_video.cc [deleted file]
src/lib/dcp_video.h [deleted file]
src/lib/dcp_video_frame.cc
src/lib/dcp_video_frame.h
src/lib/encoder.cc
src/lib/encoder.h
src/lib/ffmpeg_decoder.cc
src/lib/image.cc
src/lib/image.h
src/lib/image_decoder.cc
src/lib/image_decoder.h
src/lib/image_proxy.cc [new file with mode: 0644]
src/lib/image_proxy.h [new file with mode: 0644]
src/lib/player.cc
src/lib/player.h
src/lib/player_video_frame.cc [new file with mode: 0644]
src/lib/player_video_frame.h [new file with mode: 0644]
src/lib/server.cc
src/lib/transcoder.cc
src/lib/types.cc
src/lib/types.h
src/lib/video_content.cc
src/lib/video_decoder.cc
src/lib/video_decoder.h
src/lib/wscript
src/tools/server_test.cc
src/wx/film_viewer.cc
src/wx/film_viewer.h
test/client_server_test.cc
test/play_test.cc [new file with mode: 0644]

index 33b7e2e2122049f2fdb9bc98245bce6f6b3d9c6b..6f31dd5d24fbcd839fa07968a0e32c77d0e27977 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -2,6 +2,15 @@
 
        * Add subtitle view.
 
+2014-05-19  Carl Hetherington  <cth@carlh.net>
+
+       * Version 1.69.9 released.
+
+2014-05-19  Carl Hetherington  <cth@carlh.net>
+
+       * Decode image sources in the multi-threaded part
+       of the transcoder, rather than the single-threaded.
+
 2014-05-16  Carl Hetherington  <cth@carlh.net>
 
        * Version 1.69.8 released.
index eb01ffe49ae4603375951ef90844a4a86b445c97..9043657d831745e14cbc4ba225445807d75c4db7 100644 (file)
@@ -1,4 +1,4 @@
-dcpomatic (1.69.8-1) UNRELEASED; urgency=low
+dcpomatic (1.69.9-1) UNRELEASED; urgency=low
 
   * New upstream release.
   * New upstream release.
@@ -132,8 +132,9 @@ dcpomatic (1.69.8-1) UNRELEASED; urgency=low
   * New upstream release.
   * New upstream release.
   * New upstream release.
+  * New upstream release.
 
- -- Carl Hetherington <carl@d1stkfactory>  Fri, 16 May 2014 11:45:03 +0100
+ -- Carl Hetherington <carl@d1stkfactory>  Mon, 19 May 2014 11:16:15 +0100
 
 dcpomatic (0.87-1) UNRELEASED; urgency=low
 
index 73ee722490eaa33a7b46c1182459f9ea0164d67a..48fd6ed9c148d2fb4b3e1174e09287517c8e4363 100644 (file)
@@ -29,6 +29,7 @@
 
 using std::list;
 using std::string;
+using std::stringstream;
 using std::cout;
 using std::vector;
 using boost::shared_ptr;
index 20b5b8dec310a066eeb17e8c4a273ec32a826f2e..a7f73597c866495b0df60a2f3036bd8066d0f1f0 100644 (file)
@@ -20,7 +20,7 @@
 #ifndef DCPOMATIC_CONTENT_VIDEO_H
 #define DCPOMATIC_CONTENT_VIDEO_H
 
-class Image;
+class ImageProxy;
 
 /** @class ContentVideo
  *  @brief A frame of video straight out of some content.
@@ -32,14 +32,16 @@ public:
                : eyes (EYES_BOTH)
        {}
 
-       ContentVideo (boost::shared_ptr<const Image> i, Eyes e, VideoFrame f)
+       ContentVideo (boost::shared_ptr<const ImageProxy> i, Eyes e, Part p, VideoFrame f)
                : image (i)
                , eyes (e)
+               , part (p)
                , frame (f)
        {}
        
-       boost::shared_ptr<const Image> image;
+       boost::shared_ptr<const ImageProxy> image;
        Eyes eyes;
+       Part part;
        VideoFrame frame;
 };
 
diff --git a/src/lib/dcp_video.cc b/src/lib/dcp_video.cc
deleted file mode 100644 (file)
index 8ea5cec..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
-    Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net>
-
-    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 2 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., 675 Mass Ave, Cambridge, MA 02139, USA.
-
-*/
-
-#include "dcp_video.h"
-#include "image.h"
-
-using boost::shared_ptr;
-
-/** From ContentVideo:
- *  @param in Image.
- *  @param eyes Eye(s) that the Image is for.
- *
- *  From Content:
- *  @param crop Crop to apply.
- *  @param inter_size
- *  @param out_size
- *  @param scaler Scaler to use.
- *  @param conversion Colour conversion to use.
- *
- *  @param time DCP time.
- */
-DCPVideo::DCPVideo (
-       shared_ptr<const Image> in,
-       Eyes eyes,
-       Crop crop,
-       dcp::Size inter_size,
-       dcp::Size out_size,
-       Scaler const * scaler,
-       ColourConversion conversion,
-       DCPTime time
-       )
-       : _in (in)
-       , _eyes (eyes)
-       , _crop (crop)
-       , _inter_size (inter_size)
-       , _out_size (out_size)
-       , _scaler (scaler)
-       , _conversion (conversion)
-       , _time (time)
-{
-
-}
-
-void
-DCPVideo::set_subtitle (PositionImage s)
-{
-       _subtitle = s;
-}
-
-shared_ptr<Image>
-DCPVideo::image (AVPixelFormat format, bool aligned) const
-{
-       shared_ptr<Image> out = _in->crop_scale_window (_crop, _inter_size, _out_size, _scaler, format, aligned);
-       
-       Position<int> const container_offset ((_out_size.width - _inter_size.width) / 2, (_out_size.height - _inter_size.width) / 2);
-
-       if (_subtitle.image) {
-               out->alpha_blend (_subtitle.image, _subtitle.position);
-       }
-
-       return out;
-}
-
diff --git a/src/lib/dcp_video.h b/src/lib/dcp_video.h
deleted file mode 100644 (file)
index 75823f3..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
-    Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net>
-
-    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 2 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., 675 Mass Ave, Cambridge, MA 02139, USA.
-
-*/
-
-extern "C" {
-#include <libavutil/avutil.h>
-}
-#include <boost/shared_ptr.hpp>
-#include "types.h"
-#include "colour_conversion.h"
-#include "position.h"
-#include "position_image.h"
-
-class Image;
-class Scaler;
-
-/** @class DCPVideo
- *
- *  A ContentVideo image with:
- *     - content parameters (crop, scaling, colour conversion)
- *     - merged content (subtitles)
- *  and with its time converted from a ContentTime to a DCPTime.
- */
-class DCPVideo
-{
-public:
-       DCPVideo (boost::shared_ptr<const Image>, Eyes eyes, Crop, dcp::Size, dcp::Size, Scaler const *, ColourConversion conversion, DCPTime time);
-
-       void set_subtitle (PositionImage);
-       boost::shared_ptr<Image> image (AVPixelFormat, bool) const;
-
-       Eyes eyes () const {
-               return _eyes;
-       }
-
-       ColourConversion conversion () const {
-               return _conversion;
-       }
-
-private:
-       boost::shared_ptr<const Image> _in;
-       Eyes _eyes;
-       Crop _crop;
-       dcp::Size _inter_size;
-       dcp::Size _out_size;
-       Scaler const * _scaler;
-       ColourConversion _conversion;
-       DCPTime _time;
-       PositionImage _subtitle;
-};
index d860c319542bd52296ea0ec37f949d11e64d06dd..d154ba96b074298824da3713de768e892e6e5ed2 100644 (file)
@@ -43,6 +43,7 @@
 #include <boost/asio.hpp>
 #include <boost/filesystem.hpp>
 #include <boost/lexical_cast.hpp>
+#include <openssl/md5.h>
 #include <dcp/gamma_lut.h>
 #include <dcp/xyz_frame.h>
 #include <dcp/rgb_xyz.h>
@@ -59,6 +60,7 @@
 #include "image.h"
 #include "log.h"
 #include "cross.h"
+#include "player_video_frame.h"
 
 #include "i18n.h"
 
@@ -73,18 +75,16 @@ using dcp::raw_convert;
 #define DCI_COEFFICENT (48.0 / 52.37)
 
 /** Construct a DCP video frame.
- *  @param input Input image.
- *  @param f Index of the frame within the DCP.
+ *  @param frame Input frame.
+ *  @param index Index of the frame within the DCP.
  *  @param bw J2K bandwidth to use (see Config::j2k_bandwidth ())
  *  @param l Log to write to.
  */
 DCPVideoFrame::DCPVideoFrame (
-       shared_ptr<const Image> image, int f, Eyes eyes, ColourConversion c, int dcp_fps, int bw, Resolution r, shared_ptr<Log> l
+       shared_ptr<const PlayerVideoFrame> frame, int index, int dcp_fps, int bw, Resolution r, shared_ptr<Log> l
        )
-       : _image (image)
-       , _frame (f)
-       , _eyes (eyes)
-       , _conversion (c)
+       : _frame (frame)
+       , _index (index)
        , _frames_per_second (dcp_fps)
        , _j2k_bandwidth (bw)
        , _resolution (r)
@@ -93,22 +93,11 @@ DCPVideoFrame::DCPVideoFrame (
        
 }
 
-DCPVideoFrame::DCPVideoFrame (shared_ptr<const Image> image, cxml::ConstNodePtr node, shared_ptr<Log> log)
-       : _image (image)
+DCPVideoFrame::DCPVideoFrame (shared_ptr<const PlayerVideoFrame> frame, shared_ptr<const cxml::Node> node, shared_ptr<Log> log)
+       : _frame (frame)
        , _log (log)
 {
-       _frame = node->number_child<int> ("Frame");
-       string const eyes = node->string_child ("Eyes");
-       if (eyes == "Both") {
-               _eyes = EYES_BOTH;
-       } else if (eyes == "Left") {
-               _eyes = EYES_LEFT;
-       } else if (eyes == "Right") {
-               _eyes = EYES_RIGHT;
-       } else {
-               assert (false);
-       }
-       _conversion = ColourConversion (node->node_child ("ColourConversion"));
+       _index = node->number_child<int> ("Index");
        _frames_per_second = node->number_child<int> ("FramesPerSecond");
        _j2k_bandwidth = node->number_child<int> ("J2KBandwidth");
        _resolution = Resolution (node->optional_number_child<int>("Resolution").get_value_or (RESOLUTION_2K));
@@ -120,28 +109,44 @@ DCPVideoFrame::DCPVideoFrame (shared_ptr<const Image> image, cxml::ConstNodePtr
 shared_ptr<EncodedData>
 DCPVideoFrame::encode_locally ()
 {
-       shared_ptr<dcp::GammaLUT> in_lut;
-       in_lut = dcp::GammaLUT::cache.get (12, _conversion.input_gamma, _conversion.input_gamma_linearised);
-
+       shared_ptr<dcp::GammaLUT> in_lut = dcp::GammaLUT::cache.get (
+               12, _frame->colour_conversion().input_gamma, _frame->colour_conversion().input_gamma_linearised
+               );
+       
        /* XXX: libdcp should probably use boost */
        
        double matrix[3][3];
        for (int i = 0; i < 3; ++i) {
                for (int j = 0; j < 3; ++j) {
-                       matrix[i][j] = _conversion.matrix (i, j);
+                       matrix[i][j] = _frame->colour_conversion().matrix (i, j);
                }
        }
-       
+
        shared_ptr<dcp::XYZFrame> xyz = dcp::rgb_to_xyz (
-               _image,
+               _frame->image(),
                in_lut,
-               dcp::GammaLUT::cache.get (16, 1 / _conversion.output_gamma, false),
+               dcp::GammaLUT::cache.get (16, 1 / _frame->colour_conversion().output_gamma, false),
                matrix
                );
+
+       {
+               MD5_CTX md5_context;
+               MD5_Init (&md5_context);
+               MD5_Update (&md5_context, xyz->data(0), 1998 * 1080 * 4);
+               MD5_Update (&md5_context, xyz->data(1), 1998 * 1080 * 4);
+               MD5_Update (&md5_context, xyz->data(2), 1998 * 1080 * 4);
+               unsigned char digest[MD5_DIGEST_LENGTH];
+               MD5_Final (digest, &md5_context);
                
+               stringstream s;
+               for (int i = 0; i < MD5_DIGEST_LENGTH; ++i) {
+                       s << std::hex << std::setfill('0') << std::setw(2) << ((int) digest[i]);
+               }
+       }
+
        /* Set the max image and component sizes based on frame_rate */
        int max_cs_len = ((float) _j2k_bandwidth) / 8 / _frames_per_second;
-       if (_eyes == EYES_LEFT || _eyes == EYES_RIGHT) {
+       if (_frame->eyes() == EYES_LEFT || _frame->eyes() == EYES_RIGHT) {
                /* In 3D we have only half the normal bandwidth per eye */
                max_cs_len /= 2;
        }
@@ -240,15 +245,15 @@ DCPVideoFrame::encode_locally ()
                throw EncodeError (N_("JPEG2000 encoding failed"));
        }
 
-       switch (_eyes) {
+       switch (_frame->eyes()) {
        case EYES_BOTH:
-               _log->log (String::compose (N_("Finished locally-encoded frame %1 for mono"), _frame));
+               _log->log (String::compose (N_("Finished locally-encoded frame %1 for mono"), _index));
                break;
        case EYES_LEFT:
-               _log->log (String::compose (N_("Finished locally-encoded frame %1 for L"), _frame));
+               _log->log (String::compose (N_("Finished locally-encoded frame %1 for L"), _index));
                break;
        case EYES_RIGHT:
-               _log->log (String::compose (N_("Finished locally-encoded frame %1 for R"), _frame));
+               _log->log (String::compose (N_("Finished locally-encoded frame %1 for R"), _index));
                break;
        default:
                break;
@@ -279,28 +284,30 @@ DCPVideoFrame::encode_remotely (ServerDescription serv)
 
        socket->connect (*endpoint_iterator);
 
+       /* Collect all XML metadata */
        xmlpp::Document doc;
        xmlpp::Element* root = doc.create_root_node ("EncodingRequest");
-
        root->add_child("Version")->add_child_text (raw_convert<string> (SERVER_LINK_VERSION));
-       root->add_child("Width")->add_child_text (raw_convert<string> (_image->size().width));
-       root->add_child("Height")->add_child_text (raw_convert<string> (_image->size().height));
        add_metadata (root);
 
+       _log->log (String::compose (N_("Sending frame %1 to remote"), _index));
+       
+       /* Send XML metadata */
        stringstream xml;
        doc.write_to_stream (xml, "UTF-8");
-
-       _log->log (String::compose (N_("Sending frame %1 to remote"), _frame));
-
        socket->write (xml.str().length() + 1);
        socket->write ((uint8_t *) xml.str().c_str(), xml.str().length() + 1);
 
-       _image->write_to_socket (socket);
+       /* Send binary data */
+       _frame->send_binary (socket);
 
+       /* Read the response (JPEG2000-encoded data); this blocks until the data
+          is ready and sent back.
+       */
        shared_ptr<EncodedData> e (new RemotelyEncodedData (socket->read_uint32 ()));
        socket->read (e->data(), e->size());
 
-       _log->log (String::compose (N_("Finished remotely-encoded frame %1"), _frame));
+       _log->log (String::compose (N_("Finished remotely-encoded frame %1"), _index));
        
        return e;
 }
@@ -308,27 +315,17 @@ DCPVideoFrame::encode_remotely (ServerDescription serv)
 void
 DCPVideoFrame::add_metadata (xmlpp::Element* el) const
 {
-       el->add_child("Frame")->add_child_text (raw_convert<string> (_frame));
-
-       switch (_eyes) {
-       case EYES_BOTH:
-               el->add_child("Eyes")->add_child_text ("Both");
-               break;
-       case EYES_LEFT:
-               el->add_child("Eyes")->add_child_text ("Left");
-               break;
-       case EYES_RIGHT:
-               el->add_child("Eyes")->add_child_text ("Right");
-               break;
-       default:
-               assert (false);
-       }
-       
-       _conversion.as_xml (el->add_child("ColourConversion"));
-
+       el->add_child("Index")->add_child_text (raw_convert<string> (_index));
        el->add_child("FramesPerSecond")->add_child_text (raw_convert<string> (_frames_per_second));
        el->add_child("J2KBandwidth")->add_child_text (raw_convert<string> (_j2k_bandwidth));
        el->add_child("Resolution")->add_child_text (raw_convert<string> (int (_resolution)));
+       _frame->add_metadata (el);
+}
+
+Eyes
+DCPVideoFrame::eyes () const
+{
+       return _frame->eyes ();
 }
 
 EncodedData::EncodedData (int s)
index c51a3f02ba615c0aebdff6530577b2abe71f711f..7393efde699d1a4ad1f6560c50a102dddcb0eb8e 100644 (file)
@@ -31,6 +31,7 @@ class Scaler;
 class Image;
 class Log;
 class Subtitle;
+class PlayerVideoFrame;
 
 /** @class EncodedData
  *  @brief Container for J2K-encoded data.
@@ -100,28 +101,24 @@ public:
 class DCPVideoFrame : public boost::noncopyable
 {
 public:
-       DCPVideoFrame (boost::shared_ptr<const Image>, int, Eyes, ColourConversion, int, int, Resolution, boost::shared_ptr<Log>);
-       DCPVideoFrame (boost::shared_ptr<const Image>, cxml::ConstNodePtr, boost::shared_ptr<Log>);
+       DCPVideoFrame (boost::shared_ptr<const PlayerVideoFrame>, int, int, int, Resolution, boost::shared_ptr<Log>);
+       DCPVideoFrame (boost::shared_ptr<const PlayerVideoFrame>, cxml::ConstNodePtr, boost::shared_ptr<Log>);
 
        boost::shared_ptr<EncodedData> encode_locally ();
        boost::shared_ptr<EncodedData> encode_remotely (ServerDescription);
 
-       Eyes eyes () const {
-               return _eyes;
-       }
-       
-       int frame () const {
-               return _frame;
+       int index () const {
+               return _index;
        }
+
+       Eyes eyes () const;
        
 private:
 
        void add_metadata (xmlpp::Element *) const;
        
-       boost::shared_ptr<const Image> _image;
-       int _frame;                      ///< frame index within the DCP's intrinsic duration
-       Eyes _eyes;
-       ColourConversion _conversion;
+       boost::shared_ptr<const PlayerVideoFrame> _frame;
+       int _index;                      ///< frame index within the DCP's intrinsic duration
        int _frames_per_second;          ///< Frames per second that we will use for the DCP
        int _j2k_bandwidth;              ///< J2K bandwidth to use
        Resolution _resolution;          ///< Resolution (2K or 4K)
index b83cbc10a71a46ad434615d2bc83a0d8e303e384..2364b67a7ceccb5a1ced874efad51b83228fdcaa 100644 (file)
@@ -35,7 +35,7 @@
 #include "writer.h"
 #include "server_finder.h"
 #include "player.h"
-#include "dcp_video.h"
+#include "player_video_frame.h"
 
 #include "i18n.h"
 
@@ -123,9 +123,9 @@ Encoder::process_end ()
        */
 
        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 ()));
+               _film->log()->log (String::compose (N_("Encode left-over frame %1"), (*i)->index ()));
                try {
-                       _writer->write ((*i)->encode_locally(), (*i)->frame (), (*i)->eyes ());
+                       _writer->write ((*i)->encode_locally(), (*i)->index (), (*i)->eyes ());
                        frame_done ();
                } catch (std::exception& e) {
                        _film->log()->log (String::compose (N_("Local encode failed (%1)"), e.what ()));
@@ -178,7 +178,7 @@ Encoder::frame_done ()
 }
 
 void
-Encoder::process_video (shared_ptr<DCPVideo> frame)
+Encoder::process_video (shared_ptr<PlayerVideoFrame> pvf)
 {
        _waker.nudge ();
        
@@ -205,17 +205,15 @@ Encoder::process_video (shared_ptr<DCPVideo> frame)
        rethrow ();
 
        if (_writer->can_fake_write (_video_frames_out)) {
-               _writer->fake_write (_video_frames_out, frame->eyes ());
+               _writer->fake_write (_video_frames_out, pvf->eyes ());
                frame_done ();
        } else {
                /* Queue this new frame for encoding */
                TIMING ("adding to queue of %1", _queue.size ());
                _queue.push_back (shared_ptr<DCPVideoFrame> (
                                          new DCPVideoFrame (
-                                                 frame->image(PIX_FMT_RGB24, false),
+                                                 pvf,
                                                  _video_frames_out,
-                                                 frame->eyes(),
-                                                 frame->conversion(),
                                                  _film->video_frame_rate(),
                                                  _film->j2k_bandwidth(),
                                                  _film->resolution(),
@@ -226,7 +224,7 @@ Encoder::process_video (shared_ptr<DCPVideo> frame)
                _condition.notify_all ();
        }
 
-       if (frame->eyes() != EYES_LEFT) {
+       if (pvf->eyes() != EYES_LEFT) {
                ++_video_frames_out;
        }
 }
@@ -280,7 +278,7 @@ try
 
                TIMING ("encoder thread %1 wakes with queue of %2", boost::this_thread::get_id(), _queue.size());
                shared_ptr<DCPVideoFrame> vf = _queue.front ();
-               TIMING ("encoder thread %1 pops frame %2 (%3) from queue", boost::this_thread::get_id(), vf->frame(), vf->eyes ());
+               TIMING ("encoder thread %1 pops frame %2 (%3) from queue", boost::this_thread::get_id(), vf->index(), vf->eyes ());
                _queue.pop_front ();
                
                lock.unlock ();
@@ -306,27 +304,27 @@ try
                                _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)
+                                               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());
+                               TIMING ("encoder thread %1 begins local encode of %2", boost::this_thread::get_id(), vf->index());
                                encoded = vf->encode_locally ();
-                               TIMING ("encoder thread %1 finishes local encode of %2", boost::this_thread::get_id(), vf->frame());
+                               TIMING ("encoder thread %1 finishes local encode of %2", boost::this_thread::get_id(), vf->index());
                        } catch (std::exception& e) {
                                _film->log()->log (String::compose (N_("Local encode failed (%1)"), e.what ()));
                        }
                }
 
                if (encoded) {
-                       _writer->write (encoded, vf->frame (), vf->eyes ());
+                       _writer->write (encoded, 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())
+                               String::compose (N_("Encoder thread %1 pushes frame %2 back onto queue after failure"), boost::this_thread::get_id(), vf->index())
                                );
                        _queue.push_front (vf);
                        lock.unlock ();
index 6c465f816773d12635f1b11dd020a807c7a1b756..ac1d74c57b39af3aa449e43fd628651108701823 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
 
     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
@@ -48,7 +48,7 @@ class EncodedData;
 class Writer;
 class Job;
 class ServerFinder;
-class DCPVideo;
+class PlayerVideoFrame;
 
 /** @class Encoder
  *  @brief Encoder to J2K and WAV for DCP.
@@ -69,7 +69,7 @@ public:
        /** Call with a frame of video.
         *  @param f Video frame.
         */
-       void process_video (boost::shared_ptr<DCPVideo> f);
+       void process_video (boost::shared_ptr<PlayerVideoFrame> f);
 
        /** Call with some audio data */
        void process_audio (boost::shared_ptr<const AudioBuffers>);
index 9ae5f0485246d2f88b42432adcfcf479a1eb711a..d251a37446ff96e2e49d514575b7719e53f44b6c 100644 (file)
@@ -41,6 +41,7 @@ extern "C" {
 #include "filter_graph.h"
 #include "audio_buffers.h"
 #include "ffmpeg_content.h"
+#include "image_proxy.h"
 
 #include "i18n.h"
 
@@ -484,7 +485,7 @@ FFmpegDecoder::decode_video_packet ()
                
                if (i->second != AV_NOPTS_VALUE) {
                        double const pts = i->second * av_q2d (_format_context->streams[_video_stream]->time_base) + _pts_offset.seconds ();
-                       video (image, rint (pts * _ffmpeg_content->video_frame_rate ()));
+                       video (shared_ptr<ImageProxy> (new RawImageProxy (image)), rint (pts * _ffmpeg_content->video_frame_rate ()));
                } else {
                        _log->log ("Dropping frame without PTS");
                }
index 432cfbd54b9279e0b2631cc417b511066dc2ab2f..d4ec6f99a6381070835b2fd54a875f414e5d7f87 100644 (file)
@@ -22,6 +22,7 @@
  */
 
 #include <iostream>
+#include <openssl/md5.h>
 extern "C" {
 #include <libswscale/swscale.h>
 #include <libavutil/pixfmt.h>
@@ -40,6 +41,7 @@ using std::min;
 using std::cout;
 using std::cerr;
 using std::list;
+using std::stringstream;
 using boost::shared_ptr;
 using dcp::Size;
 
@@ -662,3 +664,24 @@ merge (list<PositionImage> images)
 
        return PositionImage (merged, all.position ());
 }
+
+string
+Image::digest () const
+{
+       MD5_CTX md5_context;
+       MD5_Init (&md5_context);
+
+       for (int i = 0; i < components(); ++i) {
+               MD5_Update (&md5_context, data()[i], line_size()[i]);
+       }
+       
+       unsigned char digest[MD5_DIGEST_LENGTH];
+       MD5_Final (digest, &md5_context);
+       
+       stringstream s;
+       for (int i = 0; i < MD5_DIGEST_LENGTH; ++i) {
+               s << std::hex << std::setfill('0') << std::setw(2) << ((int) digest[i]);
+       }
+
+       return s.str ();
+}
index 23b88dd7601066c022bb457f116b8c252c7be256..23c85e92b044d1371650805ee6364b49a9063165 100644 (file)
@@ -75,6 +75,8 @@ public:
                return _pixel_format;
        }
 
+       std::string digest () const;
+
 private:
        friend class pixel_formats_test;
        
index 5de0c8582facb1aca80ed8d03479bdf2928331d4..9f83d1d896d1444579bcd157b214b389468b9f30 100644 (file)
@@ -23,6 +23,7 @@
 #include "image_content.h"
 #include "image_decoder.h"
 #include "image.h"
+#include "image_proxy.h"
 #include "film.h"
 #include "exceptions.h"
 
@@ -46,45 +47,13 @@ ImageDecoder::pass ()
                return true;
        }
 
-       if (_image && _image_content->still ()) {
-               video (_image, _video_position);
-               ++_video_position;
-               return false;
+       if (!_image_content->still() || !_image) {
+               /* Either we need an image or we are using moving images, so load one */
+               _image.reset (new MagickImageProxy (_image_content->path (_image_content->still() ? 0 : _video_position)));
        }
-
-       Magick::Image* magick_image = 0;
-
-       boost::filesystem::path const path = _image_content->path (_image_content->still() ? 0 : _video_position);
-       
-       try {
-               magick_image = new Magick::Image (path.string ());
-       } catch (...) {
-               throw OpenFileError (path);
-       }
-       
-       dcp::Size size (magick_image->columns(), magick_image->rows());
-
-       _image.reset (new Image (PIX_FMT_RGB24, size, true));
-
-       using namespace MagickCore;
-       
-       uint8_t* p = _image->data()[0];
-       for (int y = 0; y < size.height; ++y) {
-               uint8_t* q = p;
-               for (int x = 0; x < size.width; ++x) {
-                       Magick::Color c = magick_image->pixelColor (x, y);
-                       *q++ = c.redQuantum() * 255 / QuantumRange;
-                       *q++ = c.greenQuantum() * 255 / QuantumRange;
-                       *q++ = c.blueQuantum() * 255 / QuantumRange;
-               }
-               p += _image->stride()[0];
-       }
-
-       delete magick_image;
-
+               
        video (_image, _video_position);
        ++_video_position;
-
        return false;
 }
 
index 8d88df3de44eda5536e97e9a02e5560e32084c80..242f69477826a499d915505cd0b9486808aba68d 100644 (file)
@@ -40,7 +40,7 @@ private:
        bool pass ();
        
        boost::shared_ptr<const ImageContent> _image_content;
-       boost::shared_ptr<Image> _image;
+       boost::shared_ptr<ImageProxy> _image;
        VideoFrame _video_position;
 };
 
diff --git a/src/lib/image_proxy.cc b/src/lib/image_proxy.cc
new file mode 100644 (file)
index 0000000..c74e846
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
+
+    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 2 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include <Magick++.h>
+#include <dcp/util.h>
+#include <dcp/raw_convert.h>
+#include "image_proxy.h"
+#include "image.h"
+#include "exceptions.h"
+#include "cross.h"
+
+#include "i18n.h"
+
+using std::cout;
+using std::string;
+using std::stringstream;
+using boost::shared_ptr;
+
+RawImageProxy::RawImageProxy (shared_ptr<Image> image)
+       : _image (image)
+{
+
+}
+
+RawImageProxy::RawImageProxy (shared_ptr<cxml::Node> xml, shared_ptr<Socket> socket)
+{
+       dcp::Size size (
+               xml->number_child<int> ("Width"), xml->number_child<int> ("Height")
+               );
+
+       _image.reset (new Image (PIX_FMT_RGB24, size, true));
+       _image->read_from_socket (socket);
+}
+
+shared_ptr<Image>
+RawImageProxy::image () const
+{
+       return _image;
+}
+
+void
+RawImageProxy::add_metadata (xmlpp::Node* node) const
+{
+       node->add_child("Type")->add_child_text (N_("Raw"));
+       node->add_child("Width")->add_child_text (dcp::raw_convert<string> (_image->size().width));
+       node->add_child("Height")->add_child_text (dcp::raw_convert<string> (_image->size().height));
+}
+
+void
+RawImageProxy::send_binary (shared_ptr<Socket> socket) const
+{
+       _image->write_to_socket (socket);
+}
+
+MagickImageProxy::MagickImageProxy (boost::filesystem::path path)
+{
+       /* Read the file into a Blob */
+       
+       boost::uintmax_t const size = boost::filesystem::file_size (path);
+       FILE* f = fopen_boost (path, "rb");
+       if (!f) {
+               throw OpenFileError (path);
+       }
+               
+       uint8_t* data = new uint8_t[size];
+       if (fread (data, 1, size, f) != size) {
+               delete[] data;
+               throw ReadFileError (path);
+       }
+       
+       fclose (f);
+       _blob.update (data, size);
+       delete[] data;
+}
+
+MagickImageProxy::MagickImageProxy (shared_ptr<cxml::Node>, shared_ptr<Socket> socket)
+{
+       uint32_t const size = socket->read_uint32 ();
+       uint8_t* data = new uint8_t[size];
+       socket->read (data, size);
+       _blob.update (data, size);
+       delete[] data;
+}
+
+shared_ptr<Image>
+MagickImageProxy::image () const
+{
+       if (_image) {
+               return _image;
+       }
+
+       Magick::Image* magick_image = 0;
+       try {
+               magick_image = new Magick::Image (_blob);
+       } catch (...) {
+               throw DecodeError (_("Could not decode image file"));
+       }
+
+       dcp::Size size (magick_image->columns(), magick_image->rows());
+
+       _image.reset (new Image (PIX_FMT_RGB24, size, true));
+
+       using namespace MagickCore;
+       
+       uint8_t* p = _image->data()[0];
+       for (int y = 0; y < size.height; ++y) {
+               uint8_t* q = p;
+               for (int x = 0; x < size.width; ++x) {
+                       Magick::Color c = magick_image->pixelColor (x, y);
+                       *q++ = c.redQuantum() * 255 / QuantumRange;
+                       *q++ = c.greenQuantum() * 255 / QuantumRange;
+                       *q++ = c.blueQuantum() * 255 / QuantumRange;
+               }
+               p += _image->stride()[0];
+       }
+
+       delete magick_image;
+
+       return _image;
+}
+
+void
+MagickImageProxy::add_metadata (xmlpp::Node* node) const
+{
+       node->add_child("Type")->add_child_text (N_("Magick"));
+}
+
+void
+MagickImageProxy::send_binary (shared_ptr<Socket> socket) const
+{
+       socket->write (_blob.length ());
+       socket->write ((uint8_t *) _blob.data (), _blob.length ());
+}
+
+shared_ptr<ImageProxy>
+image_proxy_factory (shared_ptr<cxml::Node> xml, shared_ptr<Socket> socket)
+{
+       if (xml->string_child("Type") == N_("Raw")) {
+               return shared_ptr<ImageProxy> (new RawImageProxy (xml, socket));
+       } else if (xml->string_child("Type") == N_("Magick")) {
+               return shared_ptr<MagickImageProxy> (new MagickImageProxy (xml, socket));
+       }
+
+       throw NetworkError (_("Unexpected image type received by server"));
+}
diff --git a/src/lib/image_proxy.h b/src/lib/image_proxy.h
new file mode 100644 (file)
index 0000000..792fa00
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
+
+    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 2 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+/** @file  src/lib/image_proxy.h
+ *  @brief ImageProxy and subclasses.
+ */
+
+#include <boost/shared_ptr.hpp>
+#include <boost/filesystem.hpp>
+#include <Magick++.h>
+#include <libxml++/libxml++.h>
+
+class Image;
+class Socket;
+
+namespace cxml {
+       class Node;
+}
+
+/** @class ImageProxy
+ *  @brief A class which holds an Image, and can produce it on request.
+ *
+ *  This is so that decoding of source images can be postponed until
+ *  the encoder thread, where multi-threading is happening, instead
+ *  of happening in a single-threaded decoder.
+ *
+ *  For example, large TIFFs are slow to decode, so this class will keep
+ *  the TIFF data TIFF until such a time that the actual image is needed.
+ *  At this point, the class decodes the TIFF to an Image.
+ */
+class ImageProxy
+{
+public:
+       virtual boost::shared_ptr<Image> image () const = 0;
+       virtual void add_metadata (xmlpp::Node *) const = 0;
+       virtual void send_binary (boost::shared_ptr<Socket>) const = 0;
+};
+
+class RawImageProxy : public ImageProxy
+{
+public:
+       RawImageProxy (boost::shared_ptr<Image>);
+       RawImageProxy (boost::shared_ptr<cxml::Node> xml, boost::shared_ptr<Socket> socket);
+
+       boost::shared_ptr<Image> image () const;
+       void add_metadata (xmlpp::Node *) const;
+       void send_binary (boost::shared_ptr<Socket>) const;
+       
+private:
+       boost::shared_ptr<Image> _image;
+};
+
+class MagickImageProxy : public ImageProxy
+{
+public:
+       MagickImageProxy (boost::filesystem::path);
+       MagickImageProxy (boost::shared_ptr<cxml::Node> xml, boost::shared_ptr<Socket> socket);
+
+       boost::shared_ptr<Image> image () const;
+       void add_metadata (xmlpp::Node *) const;
+       void send_binary (boost::shared_ptr<Socket>) const;
+
+private:       
+       Magick::Blob _blob;
+       mutable boost::shared_ptr<Image> _image;
+};
+
+boost::shared_ptr<ImageProxy> image_proxy_factory (boost::shared_ptr<cxml::Node> xml, boost::shared_ptr<Socket> socket);
index 75b5500936f9bf56fd7cb5a6bcd41ce61983bd2f..ab0d8f3566ecb484ec491ba6314ff9cbbdd5f24b 100644 (file)
 #include "playlist.h"
 #include "job.h"
 #include "image.h"
+#include "image_proxy.h"
 #include "ratio.h"
 #include "log.h"
 #include "scaler.h"
 #include "render_subtitles.h"
-#include "dcp_video.h"
 #include "config.h"
 #include "content_video.h"
+#include "player_video_frame.h"
 
 using std::list;
 using std::cout;
@@ -287,41 +288,41 @@ Player::set_approximate_size ()
        _approximate_size = true;
 }
 
-shared_ptr<DCPVideo>
-Player::black_dcp_video (DCPTime time) const
+shared_ptr<PlayerVideoFrame>
+Player::black_player_video_frame () const
 {
-       return shared_ptr<DCPVideo> (
-               new DCPVideo (
-                       _black_image,
-                       EYES_BOTH,
+       return shared_ptr<PlayerVideoFrame> (
+               new PlayerVideoFrame (
+                       shared_ptr<const ImageProxy> (new RawImageProxy (_black_image)),
                        Crop (),
                        _video_container_size,
                        _video_container_size,
                        Scaler::from_id ("bicubic"),
-                       Config::instance()->colour_conversions().front().conversion,
-                       time
+                       EYES_BOTH,
+                       PART_WHOLE,
+                       Config::instance()->colour_conversions().front().conversion
                )
        );
 }
 
-shared_ptr<DCPVideo>
-Player::content_to_dcp (
+shared_ptr<PlayerVideoFrame>
+Player::content_to_player_video_frame (
        shared_ptr<VideoContent> content,
        ContentVideo content_video,
        list<shared_ptr<Piece> > subs,
        DCPTime time,
        dcp::Size image_size) const
 {
-       shared_ptr<DCPVideo> dcp_video (
-               new DCPVideo (
+       shared_ptr<PlayerVideoFrame> pvf (
+               new PlayerVideoFrame (
                        content_video.image,
-                       content_video.eyes,
                        content->crop (),
                        image_size,
                        _video_container_size,
                        _film->scaler(),
-                       content->colour_conversion (),
-                       time
+                       content_video.eyes,
+                       content_video.part,
+                       content->colour_conversion ()
                        )
                );
        
@@ -356,14 +357,14 @@ Player::content_to_dcp (
        }
        
        if (!sub_images.empty ()) {
-               dcp_video->set_subtitle (merge (sub_images));
+               pvf->set_subtitle (merge (sub_images));
        }
 
-       return dcp_video;
+       return pvf;
 }
 
-/** @return All DCPVideo at the given time (there may be two frames for 3D) */
-list<shared_ptr<DCPVideo> >
+/** @return All PlayerVideoFrames at the given time (there may be two frames for 3D) */
+list<shared_ptr<PlayerVideoFrame> >
 Player::get_video (DCPTime time, bool accurate)
 {
        if (!_have_valid_pieces) {
@@ -375,15 +376,15 @@ Player::get_video (DCPTime time, bool accurate)
                time + DCPTime::from_frames (1, _film->video_frame_rate ())
                );
 
-       list<shared_ptr<DCPVideo> > dcp_video;
+       list<shared_ptr<PlayerVideoFrame> > pvf;
                
        if (ov.empty ()) {
                /* No video content at this time */
-               dcp_video.push_back (black_dcp_video (time));
-               return dcp_video;
+               pvf.push_back (black_player_video_frame ());
+               return pvf;
        }
 
-       /* Create a DCPVideo from the content's video at this time */
+       /* Create a PlayerVideoFrame from the content's video at this time */
 
        shared_ptr<Piece> piece = ov.back ();
        shared_ptr<VideoDecoder> decoder = dynamic_pointer_cast<VideoDecoder> (piece->decoder);
@@ -393,8 +394,8 @@ Player::get_video (DCPTime time, bool accurate)
 
        list<ContentVideo> content_video = decoder->get_video (dcp_to_content_video (piece, time), accurate);
        if (content_video.empty ()) {
-               dcp_video.push_back (black_dcp_video (time));
-               return dcp_video;
+               pvf.push_back (black_player_video_frame ());
+               return pvf;
        }
 
        dcp::Size image_size = content->scale().size (content, _video_container_size, _film->frame_size ());
@@ -409,10 +410,10 @@ Player::get_video (DCPTime time, bool accurate)
                        time + DCPTime::from_frames (1, _film->video_frame_rate ())
                        );
                
-               dcp_video.push_back (content_to_dcp (content, *i, subs, time, image_size));
+               pvf.push_back (content_to_player_video_frame (content, *i, subs, time, image_size));
        }
                
-       return dcp_video;
+       return pvf;
 }
 
 shared_ptr<AudioBuffers>
index e47cf53a1f00df21baed32027d65c50d675cee47..a96c93404e171276b7a00e0e3fdb44cb048bfba2 100644 (file)
@@ -40,9 +40,11 @@ class Playlist;
 class AudioContent;
 class Piece;
 class Image;
-class DCPVideo;
 class Decoder;
-
+class Resampler;
+class PlayerVideoFrame;
+class ImageProxy;
 class PlayerStatistics
 {
 public:
@@ -75,29 +77,6 @@ public:
        void dump (boost::shared_ptr<Log>) const;
 };
 
-/** @class PlayerImage
- *  @brief A wrapper for an Image which contains some pending operations; these may
- *  not be necessary if the receiver of the PlayerImage throws it away.
- */
-class PlayerImage
-{
-public:
-       PlayerImage (boost::shared_ptr<const Image>, Crop, dcp::Size, dcp::Size, Scaler const *);
-
-       void set_subtitle (boost::shared_ptr<const Image>, Position<int>);
-       
-       boost::shared_ptr<Image> image ();
-       
-private:
-       boost::shared_ptr<const Image> _in;
-       Crop _crop;
-       dcp::Size _inter_size;
-       dcp::Size _out_size;
-       Scaler const * _scaler;
-       boost::shared_ptr<const Image> _subtitle_image;
-       Position<int> _subtitle_position;
-};
-
 /** @class Player
  *  @brief A class which can `play' a Playlist.
  */
@@ -106,7 +85,7 @@ class Player : public boost::enable_shared_from_this<Player>, public boost::nonc
 public:
        Player (boost::shared_ptr<const Film>, boost::shared_ptr<const Playlist>);
 
-       std::list<boost::shared_ptr<DCPVideo> > get_video (DCPTime time, bool accurate);
+       std::list<boost::shared_ptr<PlayerVideoFrame> > get_video (DCPTime time, bool accurate);
        boost::shared_ptr<AudioBuffers> get_audio (DCPTime time, DCPTime length, bool accurate);
 
        void set_video_container_size (dcp::Size);
@@ -143,8 +122,8 @@ private:
        VideoFrame dcp_to_content_video (boost::shared_ptr<const Piece> piece, DCPTime t) const;
        AudioFrame dcp_to_content_audio (boost::shared_ptr<const Piece> piece, DCPTime t) const;
        ContentTime dcp_to_content_subtitle (boost::shared_ptr<const Piece> piece, DCPTime t) const;
-       boost::shared_ptr<DCPVideo> black_dcp_video (DCPTime) const;
-       boost::shared_ptr<DCPVideo> content_to_dcp (
+       boost::shared_ptr<PlayerVideoFrame> black_player_video_frame () const;
+       boost::shared_ptr<PlayerVideoFrame> content_to_player_video_frame (
                boost::shared_ptr<VideoContent> content,
                ContentVideo content_video,
                std::list<boost::shared_ptr<Piece> > subs,
diff --git a/src/lib/player_video_frame.cc b/src/lib/player_video_frame.cc
new file mode 100644 (file)
index 0000000..4258c63
--- /dev/null
@@ -0,0 +1,146 @@
+/*
+    Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net>
+
+    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 2 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include <dcp/raw_convert.h>
+#include "player_video_frame.h"
+#include "image.h"
+#include "image_proxy.h"
+#include "scaler.h"
+
+using std::string;
+using std::cout;
+using boost::shared_ptr;
+using dcp::raw_convert;
+
+PlayerVideoFrame::PlayerVideoFrame (
+       shared_ptr<const ImageProxy> in,
+       Crop crop,
+       dcp::Size inter_size,
+       dcp::Size out_size,
+       Scaler const * scaler,
+       Eyes eyes,
+       Part part,
+       ColourConversion colour_conversion
+       )
+       : _in (in)
+       , _crop (crop)
+       , _inter_size (inter_size)
+       , _out_size (out_size)
+       , _scaler (scaler)
+       , _eyes (eyes)
+       , _part (part)
+       , _colour_conversion (colour_conversion)
+{
+
+}
+
+PlayerVideoFrame::PlayerVideoFrame (shared_ptr<cxml::Node> node, shared_ptr<Socket> socket)
+{
+       _crop = Crop (node);
+
+       _inter_size = dcp::Size (node->number_child<int> ("InterWidth"), node->number_child<int> ("InterHeight"));
+       _out_size = dcp::Size (node->number_child<int> ("OutWidth"), node->number_child<int> ("OutHeight"));
+       _scaler = Scaler::from_id (node->string_child ("Scaler"));
+       _eyes = (Eyes) node->number_child<int> ("Eyes");
+       _part = (Part) node->number_child<int> ("Part");
+       _colour_conversion = ColourConversion (node);
+
+       _in = image_proxy_factory (node->node_child ("In"), socket);
+
+       if (node->optional_number_child<int> ("SubtitleX")) {
+               
+               _subtitle.position = Position<int> (node->number_child<int> ("SubtitleX"), node->number_child<int> ("SubtitleY"));
+
+               _subtitle.image.reset (
+                       new Image (PIX_FMT_RGBA, dcp::Size (node->number_child<int> ("SubtitleWidth"), node->number_child<int> ("SubtitleHeight")), true)
+                       );
+               
+               _subtitle.image->read_from_socket (socket);
+       }
+}
+
+void
+PlayerVideoFrame::set_subtitle (PositionImage image)
+{
+       _subtitle = image;
+}
+
+shared_ptr<Image>
+PlayerVideoFrame::image () const
+{
+       shared_ptr<Image> im = _in->image ();
+       
+       Crop total_crop = _crop;
+       switch (_part) {
+       case PART_LEFT_HALF:
+               total_crop.right += im->size().width / 2;
+               break;
+       case PART_RIGHT_HALF:
+               total_crop.left += im->size().width / 2;
+               break;
+       case PART_TOP_HALF:
+               total_crop.bottom += im->size().height / 2;
+               break;
+       case PART_BOTTOM_HALF:
+               total_crop.top += im->size().height / 2;
+               break;
+       default:
+               break;
+       }
+               
+       shared_ptr<Image> out = im->crop_scale_window (total_crop, _inter_size, _out_size, _scaler, PIX_FMT_RGB24, false);
+
+       Position<int> const container_offset ((_out_size.width - _inter_size.width) / 2, (_out_size.height - _inter_size.width) / 2);
+
+       if (_subtitle.image) {
+               out->alpha_blend (_subtitle.image, _subtitle.position);
+       }
+
+       return out;
+}
+
+void
+PlayerVideoFrame::add_metadata (xmlpp::Node* node) const
+{
+       _crop.as_xml (node);
+       _in->add_metadata (node->add_child ("In"));
+       node->add_child("InterWidth")->add_child_text (raw_convert<string> (_inter_size.width));
+       node->add_child("InterHeight")->add_child_text (raw_convert<string> (_inter_size.height));
+       node->add_child("OutWidth")->add_child_text (raw_convert<string> (_out_size.width));
+       node->add_child("OutHeight")->add_child_text (raw_convert<string> (_out_size.height));
+       node->add_child("Scaler")->add_child_text (_scaler->id ());
+       node->add_child("Eyes")->add_child_text (raw_convert<string> (_eyes));
+       node->add_child("Part")->add_child_text (raw_convert<string> (_part));
+       _colour_conversion.as_xml (node);
+       if (_subtitle.image) {
+               node->add_child ("SubtitleWidth")->add_child_text (raw_convert<string> (_subtitle.image->size().width));
+               node->add_child ("SubtitleHeight")->add_child_text (raw_convert<string> (_subtitle.image->size().height));
+               node->add_child ("SubtitleX")->add_child_text (raw_convert<string> (_subtitle.position.x));
+               node->add_child ("SubtitleY")->add_child_text (raw_convert<string> (_subtitle.position.y));
+       }
+}
+
+void
+PlayerVideoFrame::send_binary (shared_ptr<Socket> socket) const
+{
+       _in->send_binary (socket);
+       if (_subtitle.image) {
+               _subtitle.image->write_to_socket (socket);
+       }
+}
diff --git a/src/lib/player_video_frame.h b/src/lib/player_video_frame.h
new file mode 100644 (file)
index 0000000..225b0a4
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+    Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net>
+
+    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 2 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include <boost/shared_ptr.hpp>
+#include "types.h"
+#include "position.h"
+#include "colour_conversion.h"
+#include "position_image.h"
+
+class Image;
+class ImageProxy;
+class Scaler;
+class Socket;
+
+/** Everything needed to describe a video frame coming out of the player, but with the
+ *  bits still their raw form.  We may want to combine the bits on a remote machine,
+ *  or maybe not even bother to combine them at all.
+ */
+class PlayerVideoFrame
+{
+public:
+       PlayerVideoFrame (boost::shared_ptr<const ImageProxy>, Crop, dcp::Size, dcp::Size, Scaler const *, Eyes, Part, ColourConversion);
+       PlayerVideoFrame (boost::shared_ptr<cxml::Node>, boost::shared_ptr<Socket>);
+
+       void set_subtitle (PositionImage);
+       
+       boost::shared_ptr<Image> image () const;
+
+       void add_metadata (xmlpp::Node* node) const;
+       void send_binary (boost::shared_ptr<Socket> socket) const;
+
+       Eyes eyes () const {
+               return _eyes;
+       }
+
+       ColourConversion colour_conversion () const {
+               return _colour_conversion;
+       }
+
+private:
+       boost::shared_ptr<const ImageProxy> _in;
+       Crop _crop;
+       dcp::Size _inter_size;
+       dcp::Size _out_size;
+       Scaler const * _scaler;
+       Eyes _eyes;
+       Part _part;
+       ColourConversion _colour_conversion;
+       PositionImage _subtitle;
+};
index 6bcff7e6e93db1204cfc2d2ec73c7179a99b55ce..d72b7e5025b84e57513a97864014137d5658eacd 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
 
     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
@@ -37,6 +37,7 @@
 #include "dcp_video_frame.h"
 #include "config.h"
 #include "cross.h"
+#include "player_video_frame.h"
 
 #include "i18n.h"
 
@@ -75,7 +76,7 @@ Server::process (shared_ptr<Socket> socket, struct timeval& after_read, struct t
        uint32_t length = socket->read_uint32 ();
        scoped_array<char> buffer (new char[length]);
        socket->read (reinterpret_cast<uint8_t*> (buffer.get()), length);
-       
+
        stringstream s (buffer.get());
        shared_ptr<cxml::Document> xml (new cxml::Document ("EncodingRequest"));
        xml->read_stream (s);
@@ -85,14 +86,9 @@ Server::process (shared_ptr<Socket> socket, struct timeval& after_read, struct t
                return -1;
        }
 
-       dcp::Size size (
-               xml->number_child<int> ("Width"), xml->number_child<int> ("Height")
-               );
-
-       shared_ptr<Image> image (new Image (PIX_FMT_RGB24, size, true));
+       shared_ptr<PlayerVideoFrame> pvf (new PlayerVideoFrame (xml, socket));
 
-       image->read_from_socket (socket);
-       DCPVideoFrame dcp_video_frame (image, xml, _log);
+       DCPVideoFrame dcp_video_frame (pvf, xml, _log);
 
        gettimeofday (&after_read, 0);
        
@@ -103,15 +99,11 @@ Server::process (shared_ptr<Socket> socket, struct timeval& after_read, struct t
        try {
                encoded->send (socket);
        } catch (std::exception& e) {
-               _log->log (String::compose (
-                                  "Send failed; frame %1, data size %2, pixel format %3, image size %4x%5, %6 components",
-                                  dcp_video_frame.frame(), encoded->size(), image->pixel_format(), image->size().width, image->size().height, image->components()
-                                  )
-                       );
+               _log->log (String::compose ("Send failed; frame %1", dcp_video_frame.index()));
                throw;
        }
 
-       return dcp_video_frame.frame ();
+       return dcp_video_frame.index ();
 }
 
 void
index cc41b4256e2861630a312bd968fec4137e1dd9e9..a4cd36a4fb509f17ba6f58eae2729e0bb2901060 100644 (file)
@@ -61,8 +61,8 @@ Transcoder::go ()
 
        DCPTime const frame = DCPTime::from_frames (1, _film->video_frame_rate ());
        for (DCPTime t; t < _film->length(); t += frame) {
-               list<shared_ptr<DCPVideo> > v = _player->get_video (t, true);
-               for (list<shared_ptr<DCPVideo> >::const_iterator i = v.begin(); i != v.end(); ++i) {
+               list<shared_ptr<PlayerVideoFrame> > v = _player->get_video (t, true);
+               for (list<shared_ptr<PlayerVideoFrame> >::const_iterator i = v.begin(); i != v.end(); ++i) {
                        _encoder->process_video (*i);
                }
                _encoder->process_audio (_player->get_audio (t, frame, true));
index bc4f5f8d9d425d0d9305f1015c124ee92590dcb7..83bbf41e45c19125e779b4be18f6a48e9b2a5cd0 100644 (file)
 
 */
 
+#include <libxml++/libxml++.h>
+#include <libcxml/cxml.h>
+#include <libdcp/raw_convert.h>
 #include "types.h"
 
 using std::max;
 using std::min;
 using std::string;
+using boost::shared_ptr;
+using libdcp::raw_convert;
 
 bool operator== (Crop const & a, Crop const & b)
 {
@@ -65,3 +70,20 @@ string_to_resolution (string s)
        assert (false);
        return RESOLUTION_2K;
 }
+
+Crop::Crop (shared_ptr<cxml::Node> node)
+{
+       left = node->number_child<int> ("LeftCrop");
+       right = node->number_child<int> ("RightCrop");
+       top = node->number_child<int> ("TopCrop");
+       bottom = node->number_child<int> ("BottomCrop");
+}
+
+void
+Crop::as_xml (xmlpp::Node* node) const
+{
+       node->add_child("LeftCrop")->add_child_text (raw_convert<string> (left));
+       node->add_child("RightCrop")->add_child_text (raw_convert<string> (right));
+       node->add_child("TopCrop")->add_child_text (raw_convert<string> (top));
+       node->add_child("BottomCrop")->add_child_text (raw_convert<string> (bottom));
+}
index 35c7a91f9785865e1dada8d2449fa725f4149ae4..e858d1e1feb6b7827ce09d79822e5817b4fa3df0 100644 (file)
@@ -34,11 +34,19 @@ class SubtitleContent;
 class FFmpegContent;
 class AudioBuffers;
 
+namespace cxml {
+       class Node;
+}
+
+namespace xmlpp {
+       class Node;
+}
+
 /** The version number of the protocol used to communicate
  *  with servers.  Intended to be bumped when incompatibilities
  *  are introduced.
  */
-#define SERVER_LINK_VERSION 1
+#define SERVER_LINK_VERSION 2
 
 typedef std::vector<boost::shared_ptr<Content> > ContentList;
 typedef std::vector<boost::shared_ptr<VideoContent> > VideoContentList;
@@ -85,6 +93,15 @@ enum Eyes
        EYES_COUNT
 };
 
+enum Part
+{
+       PART_LEFT_HALF,
+       PART_RIGHT_HALF,
+       PART_TOP_HALF,
+       PART_BOTTOM_HALF,
+       PART_WHOLE
+};
+
 /** @struct Crop
  *  @brief A description of the crop of an image or video.
  */
@@ -92,6 +109,7 @@ struct Crop
 {
        Crop () : left (0), right (0), top (0), bottom (0) {}
        Crop (int l, int r, int t, int b) : left (l), right (r), top (t), bottom (b) {}
+       Crop (boost::shared_ptr<cxml::Node>);
 
        /** Number of pixels to remove from the left-hand side */
        int left;
@@ -116,6 +134,8 @@ struct Crop
                
                return s;
        }
+
+       void as_xml (xmlpp::Node *) const;
 };
 
 extern bool operator== (Crop const & a, Crop const & b);
index a67a1777ef7f5c9bea6c4876ecf4fa42ed34e10b..9c8ecf0bb7d5c9b595077ced69a80d5aef8884b9 100644 (file)
@@ -161,10 +161,7 @@ VideoContent::as_xml (xmlpp::Node* node) const
        node->add_child("VideoHeight")->add_child_text (raw_convert<string> (_video_size.height));
        node->add_child("VideoFrameRate")->add_child_text (raw_convert<string> (_video_frame_rate));
        node->add_child("VideoFrameType")->add_child_text (raw_convert<string> (static_cast<int> (_video_frame_type)));
-       node->add_child("LeftCrop")->add_child_text (raw_convert<string> (_crop.left));
-       node->add_child("RightCrop")->add_child_text (raw_convert<string> (_crop.right));
-       node->add_child("TopCrop")->add_child_text (raw_convert<string> (_crop.top));
-       node->add_child("BottomCrop")->add_child_text (raw_convert<string> (_crop.bottom));
+       _crop.as_xml (node);
        _scale.as_xml (node->add_child("Scale"));
        _colour_conversion.as_xml (node->add_child("ColourConversion"));
 }
index 1b6da8a91133a387b62813054cfd8bb2baf506eb..43b1049ccaf5f89920147f040cf5e96635f66675 100644 (file)
@@ -116,7 +116,7 @@ VideoDecoder::get_video (VideoFrame frame, bool accurate)
 
 /** Called by subclasses when they have a video frame ready */
 void
-VideoDecoder::video (shared_ptr<const Image> image, VideoFrame frame)
+VideoDecoder::video (shared_ptr<const ImageProxy> image, VideoFrame frame)
 {
        /* We should not receive the same thing twice */
        assert (_decoded_video.empty() || frame != _decoded_video.back().frame);
@@ -132,6 +132,7 @@ VideoDecoder::video (shared_ptr<const Image> image, VideoFrame frame)
                        ContentVideo (
                                _decoded_video.back().image,
                                _decoded_video.back().eyes,
+                               _decoded_video.back().part,
                                _decoded_video.back().frame + 1
                                )
                        );
@@ -139,30 +140,24 @@ VideoDecoder::video (shared_ptr<const Image> image, VideoFrame frame)
        
        switch (_video_content->video_frame_type ()) {
        case VIDEO_FRAME_TYPE_2D:
-               _decoded_video.push_back (ContentVideo (image, EYES_BOTH, frame));
+               _decoded_video.push_back (ContentVideo (image, EYES_BOTH, PART_WHOLE, frame));
                break;
        case VIDEO_FRAME_TYPE_3D_ALTERNATE:
-               _decoded_video.push_back (ContentVideo (image, (frame % 2) ? EYES_RIGHT : EYES_LEFT, frame));
+               _decoded_video.push_back (ContentVideo (image, (frame % 2) ? EYES_RIGHT : EYES_LEFT, PART_WHOLE, frame));
                break;
        case VIDEO_FRAME_TYPE_3D_LEFT_RIGHT:
-       {
-               int const half = image->size().width / 2;
-               _decoded_video.push_back (ContentVideo (image->crop (Crop (0, half, 0, 0), true), EYES_LEFT, frame));
-               _decoded_video.push_back (ContentVideo (image->crop (Crop (half, 0, 0, 0), true), EYES_RIGHT, frame));
+               _decoded_video.push_back (ContentVideo (image, EYES_LEFT, PART_LEFT_HALF, frame));
+               _decoded_video.push_back (ContentVideo (image, EYES_RIGHT, PART_RIGHT_HALF, frame));
                break;
-       }
        case VIDEO_FRAME_TYPE_3D_TOP_BOTTOM:
-       {
-               int const half = image->size().height / 2;
-               _decoded_video.push_back (ContentVideo (image->crop (Crop (0, 0, 0, half), true), EYES_LEFT, frame));
-               _decoded_video.push_back (ContentVideo (image->crop (Crop (0, 0, half, 0), true), EYES_RIGHT, frame));
+               _decoded_video.push_back (ContentVideo (image, EYES_LEFT, PART_TOP_HALF, frame));
+               _decoded_video.push_back (ContentVideo (image, EYES_RIGHT, PART_BOTTOM_HALF, frame));
                break;
-       }
        case VIDEO_FRAME_TYPE_3D_LEFT:
-               _decoded_video.push_back (ContentVideo (image, EYES_LEFT, frame));
+               _decoded_video.push_back (ContentVideo (image, EYES_LEFT, PART_WHOLE, frame));
                break;
        case VIDEO_FRAME_TYPE_3D_RIGHT:
-               _decoded_video.push_back (ContentVideo (image, EYES_RIGHT, frame));
+               _decoded_video.push_back (ContentVideo (image, EYES_RIGHT, PART_WHOLE, frame));
                break;
        default:
                assert (false);
index 145baa40b0ca115acda97f8b33376d8712890193..2c0028fd1bcfa4ce6c6a8c831452608b89467041 100644 (file)
@@ -32,7 +32,7 @@
 #include "content_video.h"
 
 class VideoContent;
-class Image;
+class ImageProxy;
 
 /** @class VideoDecoder
  *  @brief Parent for classes which decode video.
@@ -55,7 +55,7 @@ public:
 protected:
 
        void seek (ContentTime time, bool accurate);
-       void video (boost::shared_ptr<const Image>, VideoFrame frame);
+       void video (boost::shared_ptr<const ImageProxy>, VideoFrame frame);
        std::list<ContentVideo> decoded_video (VideoFrame frame);
 
        boost::shared_ptr<const VideoContent> _video_content;
index 433f50b3fdca4565902baf3b4b7e7246a59514d4..8f26c53c6e9f6c4456ee4d3123c38b2491d89e0c 100644 (file)
@@ -17,7 +17,6 @@ sources = """
           cross.cc
           dci_metadata.cc
           dcp_content_type.cc
-          dcp_video.cc
           dcp_video_frame.cc
           dcpomatic_time.cc
           dolby_cp750.cc
@@ -38,12 +37,14 @@ sources = """
           image_content.cc
           image_decoder.cc
           image_examiner.cc
+          image_proxy.cc
           job.cc
           job_manager.cc
           kdm.cc
           json_server.cc
           log.cc
           player.cc
+          player_video_frame.cc
           playlist.cc
           ratio.cc
           render_subtitles.cc
index ba16697564705fe25afe03dc2458522e4de9899a..3c2ea4b36068ecbde2eae65658cc04b75d513536 100644 (file)
@@ -34,7 +34,7 @@
 #include "lib/log.h"
 #include "lib/video_decoder.h"
 #include "lib/player.h"
-#include "lib/dcp_video.h"
+#include "lib/player_video_frame.h"
 
 using std::cout;
 using std::cerr;
@@ -48,19 +48,10 @@ static shared_ptr<FileLog> log_ (new FileLog ("servomatictest.log"));
 static int frame_count = 0;
 
 void
-process_video (shared_ptr<DCPVideo> frame)
+process_video (shared_ptr<PlayerVideoFrame> pvf)
 {
-       shared_ptr<DCPVideoFrame> local  (
-               new DCPVideoFrame (
-                       frame->image (PIX_FMT_RGB24, false), frame_count, frame->eyes(), frame->conversion(), film->video_frame_rate(), 250000000, RESOLUTION_2K, log_
-                       )
-               );
-       
-       shared_ptr<DCPVideoFrame> remote (
-               new DCPVideoFrame (
-                       frame->image (PIX_FMT_RGB24, false), frame_count, frame->eyes(), frame->conversion(), film->video_frame_rate(), 250000000, RESOLUTION_2K, log_
-                       )
-               );
+       shared_ptr<DCPVideoFrame> local  (new DCPVideoFrame (pvf, frame_count, film->video_frame_rate(), 250000000, RESOLUTION_2K, log_));
+       shared_ptr<DCPVideoFrame> remote (new DCPVideoFrame (pvf, frame_count, film->video_frame_rate(), 250000000, RESOLUTION_2K, log_));
 
        cout << "Frame " << frame_count << ": ";
        cout.flush ();
index e517c9ccaf55c2cc79c293b7a8211bbc3453271a..c848fe09b6ffa5a3f99d52f17090e837bc8e9ce3 100644 (file)
 #include "lib/examine_content_job.h"
 #include "lib/filter.h"
 #include "lib/player.h"
+#include "lib/player_video_frame.h"
 #include "lib/video_content.h"
 #include "lib/video_decoder.h"
 #include "lib/timer.h"
-#include "lib/dcp_video.h"
 #include "film_viewer.h"
 #include "wx_util.h"
 
@@ -150,9 +150,9 @@ FilmViewer::get (DCPTime p, bool accurate)
                return;
        }
 
-       list<shared_ptr<DCPVideo> > dcp_video = _player->get_video (p, accurate);
-       if (!dcp_video.empty ()) {
-               _frame = dcp_video.front()->image (PIX_FMT_BGRA, true);
+       list<shared_ptr<PlayerVideoFrame> > pvf = _player->get_video (p, accurate);
+       if (!pvf.empty ()) {
+               _frame = pvf.front()->image ();
                _frame = _frame->scale (_frame->size(), Scaler::from_id ("fastbilinear"), PIX_FMT_RGB24, false);
        } else {
                _frame.reset ();
index 207004f29715b6a5958b478df586f24fb5124719..950758f61c9f401cd63e28a4473f013e8ab18785 100644 (file)
@@ -28,7 +28,7 @@ class wxToggleButton;
 class FFmpegPlayer;
 class Image;
 class RGBPlusAlphaImage;
-class PlayerImage;
+class PlayerVideoFrame;
 
 /** @class FilmViewer
  *  @brief A wx widget to view a preview of a Film.
index a459e6c71449721620c61ea473c8287b4facb446..8e4fb0e1867e845e644864012eae8812b2468183 100644 (file)
@@ -31,6 +31,9 @@
 #include "lib/image.h"
 #include "lib/cross.h"
 #include "lib/dcp_video_frame.h"
+#include "lib/scaler.h"
+#include "lib/player_video_frame.h"
+#include "lib/image_proxy.h"
 
 using std::list;
 using boost::shared_ptr;
@@ -75,17 +78,27 @@ BOOST_AUTO_TEST_CASE (client_server_test)
                p += sub_image->stride()[0];
        }
 
-       /* XXX */
-//     shared_ptr<Subtitle> subtitle (new Subtitle (Position<int> (50, 60), sub_image));
+       shared_ptr<PlayerVideoFrame> pvf (
+               new PlayerVideoFrame (
+                       shared_ptr<ImageProxy> (new RawImageProxy (image)),
+                       Crop (),
+                       dcp::Size (1998, 1080),
+                       dcp::Size (1998, 1080),
+                       Scaler::from_id ("bicubic"),
+                       EYES_BOTH,
+                       PART_WHOLE,
+                       ColourConversion ()
+                       )
+               );
+
+       pvf->set_subtitle (PositionImage (sub_image, Position<int> (50, 60)));
 
        shared_ptr<FileLog> log (new FileLog ("build/test/client_server_test.log"));
 
        shared_ptr<DCPVideoFrame> frame (
                new DCPVideoFrame (
-                       image,
+                       pvf,
                        0,
-                       EYES_BOTH,
-                       ColourConversion (),
                        24,
                        200000000,
                        RESOLUTION_2K,
diff --git a/test/play_test.cc b/test/play_test.cc
new file mode 100644 (file)
index 0000000..1ba2e7d
--- /dev/null
@@ -0,0 +1,129 @@
+/*
+    Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net>
+
+    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 2 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include <boost/test/unit_test.hpp>
+#include "lib/player.h"
+#include "lib/ratio.h"
+#include "lib/dcp_content_type.h"
+#include "lib/player_video_frame.h"
+#include "test.h"
+
+/* This test needs stuff in Player that is only included in debug mode */
+#ifdef DCPOMATIC_DEBUG
+
+using std::cout;
+using boost::optional;
+using boost::shared_ptr;
+
+struct Video
+{
+       boost::shared_ptr<Content> content;
+       boost::shared_ptr<const Image> image;
+       Time time;
+};
+
+class PlayerWrapper
+{
+public:
+       PlayerWrapper (shared_ptr<Player> p)
+               : _player (p)
+       {
+               _player->Video.connect (bind (&PlayerWrapper::process_video, this, _1, _3));
+       }
+
+       void process_video (shared_ptr<PlayerVideoFrame> i, Time t)
+       {
+               Video v;
+               v.content = _player->_last_video;
+               v.image = i->image ();
+               v.time = t;
+               _queue.push_front (v);
+       }
+
+       optional<Video> get_video ()
+       {
+               while (_queue.empty() && !_player->pass ()) {}
+               if (_queue.empty ()) {
+                       return optional<Video> ();
+               }
+               
+               Video v = _queue.back ();
+               _queue.pop_back ();
+               return v;
+       }
+
+       void seek (Time t, bool ac)
+       {
+               _player->seek (t, ac);
+               _queue.clear ();
+       }
+
+private:
+       shared_ptr<Player> _player;
+       std::list<Video> _queue;
+};
+
+BOOST_AUTO_TEST_CASE (play_test)
+{
+       shared_ptr<Film> film = new_test_film ("play_test");
+       film->set_dcp_content_type (DCPContentType::from_dci_name ("FTR"));
+       film->set_container (Ratio::from_id ("185"));
+       film->set_name ("play_test");
+
+       shared_ptr<FFmpegContent> A (new FFmpegContent (film, "test/data/red_24.mp4"));
+       film->examine_and_add_content (A);
+       wait_for_jobs ();
+
+       BOOST_CHECK_EQUAL (A->video_length_after_3d_combine(), 16);
+
+       shared_ptr<FFmpegContent> B (new FFmpegContent (film, "test/data/red_30.mp4"));
+       film->examine_and_add_content (B);
+       wait_for_jobs ();
+
+       BOOST_CHECK_EQUAL (B->video_length_after_3d_combine(), 16);
+       
+       /* Film should have been set to 25fps */
+       BOOST_CHECK_EQUAL (film->video_frame_rate(), 25);
+
+       BOOST_CHECK_EQUAL (A->position(), 0);
+       /* A is 16 frames long at 25 fps */
+       BOOST_CHECK_EQUAL (B->position(), 16 * TIME_HZ / 25);
+
+       shared_ptr<Player> player = film->make_player ();
+       PlayerWrapper wrap (player);
+       /* Seek and audio don't get on at the moment */
+       player->disable_audio ();
+
+       for (int i = 0; i < 32; ++i) {
+               optional<Video> v = wrap.get_video ();
+               BOOST_CHECK (v);
+               if (i < 16) {
+                       BOOST_CHECK (v.get().content == A);
+               } else {
+                       BOOST_CHECK (v.get().content == B);
+               }
+       }
+
+       player->seek (10 * TIME_HZ / 25, true);
+       optional<Video> v = wrap.get_video ();
+       BOOST_CHECK (v);
+       BOOST_CHECK_EQUAL (v.get().time, 10 * TIME_HZ / 25);
+}
+
+#endif