X-Git-Url: https://main.carlh.net/gitweb/?a=blobdiff_plain;f=src%2Flib%2Fplayer.cc;h=5b449ccc1bda6172f33de161ba9f87f0054c6424;hb=28111007e2e6fd62f5810be780706ae1618bd33f;hp=68b3365ea5845eb9ddfce38a4c5ba622da4d880a;hpb=0a5e92450ae992e9ed24026dd60ae351d0a3e60b;p=dcpomatic.git diff --git a/src/lib/player.cc b/src/lib/player.cc index 68b3365ea..5b449ccc1 100644 --- a/src/lib/player.cc +++ b/src/lib/player.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2018 Carl Hetherington + Copyright (C) 2013-2020 Carl Hetherington This file is part of DCP-o-matic. @@ -18,11 +18,13 @@ */ +#include "atmos_decoder.h" #include "player.h" #include "film.h" #include "audio_buffers.h" #include "content_audio.h" #include "dcp_content.h" +#include "dcpomatic_log.h" #include "job.h" #include "image.h" #include "raw_image_proxy.h" @@ -48,20 +50,18 @@ #include "image_decoder.h" #include "compose.hpp" #include "shuffler.h" +#include "timer.h" #include #include #include #include #include -#include #include #include #include #include "i18n.h" -#define LOG_GENERAL(...) _film->log()->log (String::compose (__VA_ARGS__), LogEntry::TYPE_GENERAL); - using std::list; using std::cout; using std::min; @@ -72,38 +72,70 @@ using std::pair; using std::map; using std::make_pair; using std::copy; -using boost::shared_ptr; -using boost::weak_ptr; -using boost::dynamic_pointer_cast; +using std::shared_ptr; +using std::weak_ptr; +using std::dynamic_pointer_cast; using boost::optional; using boost::scoped_ptr; +#if BOOST_VERSION >= 106100 +using namespace boost::placeholders; +#endif +using namespace dcpomatic; int const PlayerProperty::VIDEO_CONTAINER_SIZE = 700; int const PlayerProperty::PLAYLIST = 701; int const PlayerProperty::FILM_CONTAINER = 702; int const PlayerProperty::FILM_VIDEO_FRAME_RATE = 703; int const PlayerProperty::DCP_DECODE_REDUCTION = 704; +int const PlayerProperty::PLAYBACK_LENGTH = 705; -Player::Player (shared_ptr film, shared_ptr playlist) +Player::Player (shared_ptr film) : _film (film) - , _playlist (playlist) - , _have_valid_pieces (false) + , _suspended (0) , _ignore_video (false) , _ignore_audio (false) , _ignore_text (false) , _always_burn_open_subtitles (false) , _fast (false) + , _tolerant (film->tolerant()) , _play_referenced (false) , _audio_merger (_film->audio_frame_rate()) , _shuffler (0) { - _film_changed_connection = _film->Changed.connect (bind (&Player::film_changed, this, _1)); - _playlist_changed_connection = _playlist->Changed.connect (bind (&Player::playlist_changed, this)); - _playlist_content_changed_connection = _playlist->ContentChanged.connect (bind (&Player::playlist_content_changed, this, _1, _2, _3)); + construct (); +} + +Player::Player (shared_ptr film, shared_ptr playlist_) + : _film (film) + , _playlist (playlist_) + , _suspended (0) + , _ignore_video (false) + , _ignore_audio (false) + , _ignore_text (false) + , _always_burn_open_subtitles (false) + , _fast (false) + , _tolerant (film->tolerant()) + , _play_referenced (false) + , _audio_merger (_film->audio_frame_rate()) + , _shuffler (0) +{ + construct (); +} + +void +Player::construct () +{ + _film_changed_connection = _film->Change.connect (bind (&Player::film_change, this, _1, _2)); + /* The butler must hear about this first, so since we are proxying this through to the butler we must + be first. + */ + _playlist_change_connection = playlist()->Change.connect (bind (&Player::playlist_change, this, _1), boost::signals2::at_front); + _playlist_content_change_connection = playlist()->ContentChange.connect (bind(&Player::playlist_content_change, this, _1, _3, _4)); set_video_container_size (_film->frame_size ()); - film_changed (Film::AUDIO_PROCESSOR); + film_change (CHANGE_TYPE_DONE, Film::AUDIO_PROCESSOR); + setup_pieces (); seek (DCPTime (), true); } @@ -115,13 +147,36 @@ Player::~Player () void Player::setup_pieces () { + boost::mutex::scoped_lock lm (_mutex); + setup_pieces_unlocked (); +} + + +bool +have_video (shared_ptr content) +{ + return static_cast(content->video) && content->video->use(); +} + +bool +have_audio (shared_ptr content) +{ + return static_cast(content->audio); +} + +void +Player::setup_pieces_unlocked () +{ + _playback_length = _playlist ? _playlist->length(_film) : _film->length(); + + list > old_pieces = _pieces; _pieces.clear (); delete _shuffler; _shuffler = new Shuffler(); _shuffler->Video.connect(bind(&Player::video, this, _1, _2)); - BOOST_FOREACH (shared_ptr i, _playlist->content ()) { + for (auto i: playlist()->content()) { if (!i->paths_valid ()) { continue; @@ -132,14 +187,19 @@ Player::setup_pieces () continue; } - shared_ptr decoder = decoder_factory (i, _film->log(), _fast); - FrameRateChange frc (i->active_video_frame_rate(), _film->video_frame_rate()); - - if (!decoder) { - /* Not something that we can decode; e.g. Atmos content */ - continue; + shared_ptr old_decoder; + for (auto j: old_pieces) { + if (j->content == i) { + old_decoder = j->decoder; + break; + } } + shared_ptr decoder = decoder_factory (_film, i, _fast, _tolerant, old_decoder); + DCPOMATIC_ASSERT (decoder); + + FrameRateChange frc (_film, i); + if (decoder->video && _ignore_video) { decoder->video->set_ignore (true); } @@ -149,7 +209,7 @@ Player::setup_pieces () } if (_ignore_text) { - BOOST_FOREACH (shared_ptr i, decoder->text) { + for (auto i: decoder->text) { i->set_ignore (true); } } @@ -188,94 +248,75 @@ Player::setup_pieces () bind(&Player::plain_text_start, this, weak_ptr(piece), weak_ptr((*j)->content()), _1) ); (*j)->Stop.connect ( - bind(&Player::subtitle_stop, this, weak_ptr(piece), weak_ptr((*j)->content()), _1, _2) + bind(&Player::subtitle_stop, this, weak_ptr(piece), weak_ptr((*j)->content()), _1) ); ++j; } + + if (decoder->atmos) { + decoder->atmos->Data.connect (bind(&Player::atmos, this, weak_ptr(piece), _1)); + } } _stream_states.clear (); - BOOST_FOREACH (shared_ptr i, _pieces) { + for (auto i: _pieces) { if (i->content->audio) { - BOOST_FOREACH (AudioStreamPtr j, i->content->audio->streams()) { + for (auto j: i->content->audio->streams()) { _stream_states[j] = StreamState (i, i->content->position ()); } } } - _black = Empty (_film->content(), _film->length(), bind(&Content::video, _1)); - _silent = Empty (_film->content(), _film->length(), bind(&Content::audio, _1)); + _black = Empty (_film, playlist(), bind(&have_video, _1), _playback_length); + _silent = Empty (_film, playlist(), bind(&have_audio, _1), _playback_length); _last_video_time = DCPTime (); _last_video_eyes = EYES_BOTH; _last_audio_time = DCPTime (); - _have_valid_pieces = true; } void -Player::playlist_content_changed (weak_ptr w, int property, bool frequent) +Player::playlist_content_change (ChangeType type, int property, bool frequent) { - shared_ptr c = w.lock (); - if (!c) { - return; - } - - if ( - property == ContentProperty::POSITION || - property == ContentProperty::LENGTH || - property == ContentProperty::TRIM_START || - property == ContentProperty::TRIM_END || - property == ContentProperty::PATH || - property == VideoContentProperty::FRAME_TYPE || - property == VideoContentProperty::COLOUR_CONVERSION || - property == AudioContentProperty::STREAMS || - property == DCPContentProperty::NEEDS_ASSETS || - property == DCPContentProperty::NEEDS_KDM || - property == TextContentProperty::COLOUR || - property == TextContentProperty::EFFECT || - property == TextContentProperty::EFFECT_COLOUR || - property == FFmpegContentProperty::SUBTITLE_STREAM || - property == FFmpegContentProperty::FILTERS - ) { - - { + if (property == VideoContentProperty::CROP) { + if (type == CHANGE_TYPE_DONE) { + dcp::Size const vcs = video_container_size(); boost::mutex::scoped_lock lm (_mutex); - _have_valid_pieces = false; + for (list, DCPTime> >::const_iterator i = _delay.begin(); i != _delay.end(); ++i) { + i->first->reset_metadata (_film, vcs); + } + } + } else { + if (type == CHANGE_TYPE_PENDING) { + /* The player content is probably about to change, so we can't carry on + until that has happened and we've rebuilt our pieces. Stop pass() + and seek() from working until then. + */ + ++_suspended; + } else if (type == CHANGE_TYPE_DONE) { + /* A change in our content has gone through. Re-build our pieces. */ + setup_pieces (); + --_suspended; + } else if (type == CHANGE_TYPE_CANCELLED) { + --_suspended; } - - Changed (property, frequent); - - } else if ( - property == TextContentProperty::LINE_SPACING || - property == TextContentProperty::OUTLINE_WIDTH || - property == TextContentProperty::Y_SCALE || - property == TextContentProperty::FADE_IN || - property == TextContentProperty::FADE_OUT || - property == ContentProperty::VIDEO_FRAME_RATE || - property == TextContentProperty::USE || - property == TextContentProperty::X_OFFSET || - property == TextContentProperty::Y_OFFSET || - property == TextContentProperty::X_SCALE || - property == TextContentProperty::FONTS || - property == TextContentProperty::TYPE || - property == VideoContentProperty::CROP || - property == VideoContentProperty::SCALE || - property == VideoContentProperty::FADE_IN || - property == VideoContentProperty::FADE_OUT - ) { - - Changed (property, frequent); } + + Change (type, property, frequent); } void Player::set_video_container_size (dcp::Size s) { + Change (CHANGE_TYPE_PENDING, PlayerProperty::VIDEO_CONTAINER_SIZE, false); + { boost::mutex::scoped_lock lm (_mutex); if (s == _video_container_size) { + lm.unlock (); + Change (CHANGE_TYPE_CANCELLED, PlayerProperty::VIDEO_CONTAINER_SIZE, false); return; } @@ -285,22 +326,20 @@ Player::set_video_container_size (dcp::Size s) _black_image->make_black (); } - Changed (PlayerProperty::VIDEO_CONTAINER_SIZE, false); + Change (CHANGE_TYPE_DONE, PlayerProperty::VIDEO_CONTAINER_SIZE, false); } void -Player::playlist_changed () +Player::playlist_change (ChangeType type) { - { - boost::mutex::scoped_lock lm (_mutex); - _have_valid_pieces = false; + if (type == CHANGE_TYPE_DONE) { + setup_pieces (); } - - Changed (PlayerProperty::PLAYLIST, false); + Change (type, PlayerProperty::PLAYLIST, false); } void -Player::film_changed (Film::Property p) +Player::film_change (ChangeType type, Film::Property p) { /* Here we should notice Film properties that affect our output, and alert listeners that our output now would be different to how it was @@ -308,58 +347,26 @@ Player::film_changed (Film::Property p) */ if (p == Film::CONTAINER) { - Changed (PlayerProperty::FILM_CONTAINER, false); + Change (type, PlayerProperty::FILM_CONTAINER, false); } else if (p == Film::VIDEO_FRAME_RATE) { /* Pieces contain a FrameRateChange which contains the DCP frame rate, so we need new pieces here. */ - { - boost::mutex::scoped_lock lm (_mutex); - _have_valid_pieces = false; + if (type == CHANGE_TYPE_DONE) { + setup_pieces (); } - Changed (PlayerProperty::FILM_VIDEO_FRAME_RATE, false); + Change (type, PlayerProperty::FILM_VIDEO_FRAME_RATE, false); } else if (p == Film::AUDIO_PROCESSOR) { - if (_film->audio_processor ()) { + if (type == CHANGE_TYPE_DONE && _film->audio_processor ()) { boost::mutex::scoped_lock lm (_mutex); _audio_processor = _film->audio_processor()->clone (_film->audio_frame_rate ()); } } else if (p == Film::AUDIO_CHANNELS) { - boost::mutex::scoped_lock lm (_mutex); - _audio_merger.clear (); - } -} - -list -Player::transform_bitmap_texts (list subs) const -{ - list all; - - for (list::const_iterator i = subs.begin(); i != subs.end(); ++i) { - if (!i->image) { - continue; + if (type == CHANGE_TYPE_DONE) { + boost::mutex::scoped_lock lm (_mutex); + _audio_merger.clear (); } - - /* We will scale the subtitle up to fit _video_container_size */ - dcp::Size scaled_size (i->rectangle.width * _video_container_size.width, i->rectangle.height * _video_container_size.height); - - all.push_back ( - PositionImage ( - i->image->scale ( - scaled_size, - dcp::YUV_TO_RGB_REC601, - i->image->pixel_format (), - true, - _fast - ), - Position ( - lrint (_video_container_size.width * i->rectangle.x), - lrint (_video_container_size.height * i->rectangle.y) - ) - ) - ); } - - return all; } shared_ptr @@ -375,8 +382,10 @@ Player::black_player_video_frame (Eyes eyes) const eyes, PART_WHOLE, PresetColourConversion::all().front().conversion, - boost::weak_ptr(), - boost::optional() + VIDEO_RANGE_FULL, + std::weak_ptr(), + boost::optional(), + false ) ); } @@ -385,7 +394,7 @@ Frame Player::dcp_to_content_video (shared_ptr piece, DCPTime t) const { DCPTime s = t - piece->content->position (); - s = min (piece->content->length_after_trim(), s); + s = min (piece->content->length_after_trim(_film), s); s = max (DCPTime(), s + DCPTime (piece->content->trim_start(), piece->frc)); /* It might seem more logical here to convert s to a ContentTime (using the FrameRateChange) @@ -410,7 +419,7 @@ Frame Player::dcp_to_resampled_audio (shared_ptr piece, DCPTime t) const { DCPTime s = t - piece->content->position (); - s = min (piece->content->length_after_trim(), s); + s = min (piece->content->length_after_trim(_film), s); /* See notes in dcp_to_content_video */ return max (DCPTime (), DCPTime (piece->content->trim_start (), piece->frc) + s).frames_floor (_film->audio_frame_rate ()); } @@ -428,7 +437,7 @@ ContentTime Player::dcp_to_content_time (shared_ptr piece, DCPTime t) const { DCPTime s = t - piece->content->position (); - s = min (piece->content->length_after_trim(), s); + s = min (piece->content->length_after_trim(_film), s); return max (ContentTime (), ContentTime (s, piece->frc) + piece->content->trim_start()); } @@ -438,24 +447,18 @@ Player::content_time_to_dcp (shared_ptr piece, ContentTime t) const return max (DCPTime (), DCPTime (t - piece->content->trim_start(), piece->frc) + piece->content->position()); } -list > +vector Player::get_subtitle_fonts () { - /* Does not require a lock on _mutex as it's only called from DCPEncoder */ - - if (!_have_valid_pieces) { - setup_pieces (); - } + boost::mutex::scoped_lock lm (_mutex); - list > fonts; - BOOST_FOREACH (shared_ptr i, _pieces) { - BOOST_FOREACH (shared_ptr j, i->content->text) { - /* XXX: things may go wrong if there are duplicate font IDs - with different font files. - */ - list > f = j->fonts (); - copy (f.begin(), f.end(), back_inserter (fonts)); - } + vector fonts; + for (auto i: _pieces) { + /* XXX: things may go wrong if there are duplicate font IDs + with different font files. + */ + vector f = i->decoder->fonts (); + copy (f.begin(), f.end(), back_inserter(fonts)); } return fonts; @@ -467,7 +470,7 @@ Player::set_ignore_video () { boost::mutex::scoped_lock lm (_mutex); _ignore_video = true; - _have_valid_pieces = false; + setup_pieces_unlocked (); } void @@ -475,7 +478,7 @@ Player::set_ignore_audio () { boost::mutex::scoped_lock lm (_mutex); _ignore_audio = true; - _have_valid_pieces = false; + setup_pieces_unlocked (); } void @@ -483,6 +486,7 @@ Player::set_ignore_text () { boost::mutex::scoped_lock lm (_mutex); _ignore_text = true; + setup_pieces_unlocked (); } /** Set the player to always burn open texts into the image regardless of the content settings */ @@ -499,7 +503,7 @@ Player::set_fast () { boost::mutex::scoped_lock lm (_mutex); _fast = true; - _have_valid_pieces = false; + setup_pieces_unlocked (); } void @@ -507,7 +511,20 @@ Player::set_play_referenced () { boost::mutex::scoped_lock lm (_mutex); _play_referenced = true; - _have_valid_pieces = false; + setup_pieces_unlocked (); +} + +static void +maybe_add_asset (list& a, shared_ptr r, Frame reel_trim_start, Frame reel_trim_end, DCPTime from, int const ffr) +{ + DCPOMATIC_ASSERT (r); + r->set_entry_point (r->entry_point().get_value_or(0) + reel_trim_start); + r->set_duration (r->actual_duration() - reel_trim_start - reel_trim_end); + if (r->actual_duration() > 0) { + a.push_back ( + ReferencedReelAsset(r, DCPTimePeriod(from, from + DCPTime::from_frames(r->actual_duration(), ffr))) + ); + } } list @@ -517,7 +534,7 @@ Player::get_reel_assets () list a; - BOOST_FOREACH (shared_ptr i, _playlist->content ()) { + for (auto i: playlist()->content()) { shared_ptr j = dynamic_pointer_cast (i); if (!j) { continue; @@ -525,63 +542,56 @@ Player::get_reel_assets () scoped_ptr decoder; try { - decoder.reset (new DCPDecoder (j, _film->log(), false)); + decoder.reset (new DCPDecoder (_film, j, false, false, shared_ptr())); } catch (...) { return a; } - int64_t offset = 0; - BOOST_FOREACH (shared_ptr k, decoder->reels()) { + DCPOMATIC_ASSERT (j->video_frame_rate ()); + double const cfr = j->video_frame_rate().get(); + Frame const trim_start = j->trim_start().frames_round (cfr); + Frame const trim_end = j->trim_end().frames_round (cfr); + int const ffr = _film->video_frame_rate (); + + /* position in the asset from the start */ + int64_t offset_from_start = 0; + /* position in the asset from the end */ + int64_t offset_from_end = 0; + for (auto k: decoder->reels()) { + /* Assume that main picture duration is the length of the reel */ + offset_from_end += k->main_picture()->actual_duration(); + } + + for (auto k: decoder->reels()) { - DCPOMATIC_ASSERT (j->video_frame_rate ()); - double const cfr = j->video_frame_rate().get(); - Frame const trim_start = j->trim_start().frames_round (cfr); - Frame const trim_end = j->trim_end().frames_round (cfr); - int const ffr = _film->video_frame_rate (); + /* Assume that main picture duration is the length of the reel */ + int64_t const reel_duration = k->main_picture()->actual_duration(); - DCPTime const from = i->position() + DCPTime::from_frames (offset, _film->video_frame_rate()); + /* See doc/design/trim_reels.svg */ + Frame const reel_trim_start = min(reel_duration, max(int64_t(0), trim_start - offset_from_start)); + Frame const reel_trim_end = min(reel_duration, max(int64_t(0), reel_duration - (offset_from_end - trim_end))); + + DCPTime const from = i->position() + DCPTime::from_frames (offset_from_start, _film->video_frame_rate()); if (j->reference_video ()) { - shared_ptr ra = k->main_picture (); - DCPOMATIC_ASSERT (ra); - ra->set_entry_point (ra->entry_point() + trim_start); - ra->set_duration (ra->duration() - trim_start - trim_end); - a.push_back ( - ReferencedReelAsset (ra, DCPTimePeriod (from, from + DCPTime::from_frames (ra->duration(), ffr))) - ); + maybe_add_asset (a, k->main_picture(), reel_trim_start, reel_trim_end, from, ffr); } if (j->reference_audio ()) { - shared_ptr ra = k->main_sound (); - DCPOMATIC_ASSERT (ra); - ra->set_entry_point (ra->entry_point() + trim_start); - ra->set_duration (ra->duration() - trim_start - trim_end); - a.push_back ( - ReferencedReelAsset (ra, DCPTimePeriod (from, from + DCPTime::from_frames (ra->duration(), ffr))) - ); + maybe_add_asset (a, k->main_sound(), reel_trim_start, reel_trim_end, from, ffr); } if (j->reference_text (TEXT_OPEN_SUBTITLE)) { - shared_ptr ra = k->main_subtitle (); - DCPOMATIC_ASSERT (ra); - ra->set_entry_point (ra->entry_point() + trim_start); - ra->set_duration (ra->duration() - trim_start - trim_end); - a.push_back ( - ReferencedReelAsset (ra, DCPTimePeriod (from, from + DCPTime::from_frames (ra->duration(), ffr))) - ); + maybe_add_asset (a, k->main_subtitle(), reel_trim_start, reel_trim_end, from, ffr); } if (j->reference_text (TEXT_CLOSED_CAPTION)) { - shared_ptr ra = k->closed_caption (); - DCPOMATIC_ASSERT (ra); - ra->set_entry_point (ra->entry_point() + trim_start); - ra->set_duration (ra->duration() - trim_start - trim_end); - a.push_back ( - ReferencedReelAsset (ra, DCPTimePeriod (from, from + DCPTime::from_frames (ra->duration(), ffr))) - ); + for (auto l: k->closed_captions()) { + maybe_add_asset (a, l, reel_trim_start, reel_trim_end, from, ffr); + } } - /* Assume that main picture duration is the length of the reel */ - offset += k->main_picture()->duration (); + offset_from_start += reel_duration; + offset_from_end -= reel_duration; } } @@ -593,12 +603,14 @@ Player::pass () { boost::mutex::scoped_lock lm (_mutex); - if (!_have_valid_pieces) { - setup_pieces (); + if (_suspended) { + /* We can't pass in this state */ + LOG_DEBUG_PLAYER_NC ("Player is suspended"); + return false; } - if (_playlist->length() == DCPTime()) { - /* Special case of an empty Film; just give one black frame */ + if (_playback_length == DCPTime()) { + /* Special; just give one black frame */ emit_video (black_player_video_frame(EYES_BOTH), DCPTime()); return true; } @@ -608,13 +620,13 @@ Player::pass () shared_ptr earliest_content; optional earliest_time; - BOOST_FOREACH (shared_ptr i, _pieces) { + for (auto i: _pieces) { if (i->done) { continue; } DCPTime const t = content_time_to_dcp (i, max(i->decoder->position(), i->content->trim_start())); - if (t > i->content->end()) { + if (t > i->content->end(_film)) { i->done = true; } else { @@ -641,34 +653,54 @@ Player::pass () which = CONTENT; } - if (!_black.done() && (!earliest_time || _black.position() < *earliest_time)) { + if (!_black.done() && !_ignore_video && (!earliest_time || _black.position() < *earliest_time)) { earliest_time = _black.position (); which = BLACK; } - if (!_silent.done() && (!earliest_time || _silent.position() < *earliest_time)) { + if (!_silent.done() && !_ignore_audio && (!earliest_time || _silent.position() < *earliest_time)) { earliest_time = _silent.position (); which = SILENT; } switch (which) { case CONTENT: + { + LOG_DEBUG_PLAYER ("Calling pass() on %1", earliest_content->content->path(0)); earliest_content->done = earliest_content->decoder->pass (); + shared_ptr dcp = dynamic_pointer_cast(earliest_content->content); + if (dcp && !_play_referenced && dcp->reference_audio()) { + /* We are skipping some referenced DCP audio content, so we need to update _last_audio_time + to `hide' the fact that no audio was emitted during the referenced DCP (though + we need to behave as though it was). + */ + _last_audio_time = dcp->end (_film); + } break; + } case BLACK: + LOG_DEBUG_PLAYER ("Emit black for gap at %1", to_string(_black.position())); emit_video (black_player_video_frame(EYES_BOTH), _black.position()); _black.set_position (_black.position() + one_video_frame()); break; case SILENT: { + LOG_DEBUG_PLAYER ("Emit silence for gap at %1", to_string(_silent.position())); DCPTimePeriod period (_silent.period_at_position()); if (_last_audio_time) { /* Sometimes the thing that happened last finishes fractionally before - this silence. Bodge the start time of the silence to fix it. I'm - not sure if this is the right solution --- maybe the last thing should - be padded `forward' rather than this thing padding `back'. + or after this silence. Bodge the start time of the silence to fix it. + I think this is nothing to worry about since we will just add or + remove a little silence at the end of some content. */ - period.from = min(period.from, *_last_audio_time); + int64_t const error = labs(period.from.get() - _last_audio_time->get()); + /* Let's not worry about less than a frame at 24fps */ + int64_t const too_much_error = DCPTime::from_frames(1, 24).get(); + if (error >= too_much_error) { + _film->log()->log(String::compose("Silence starting before or after last audio by %1", error), LogEntry::TYPE_ERROR); + } + DCPOMATIC_ASSERT (error < too_much_error); + period.from = *_last_audio_time; } if (period.duration() > one_video_frame()) { period.to = period.from + one_video_frame(); @@ -687,7 +719,7 @@ Player::pass () /* Work out the time before which the audio is definitely all here. This is the earliest last_push_end of one of our streams, or the position of the _silent. */ - DCPTime pull_to = _film->length (); + DCPTime pull_to = _playback_length; for (map::const_iterator i = _stream_states.begin(); i != _stream_states.end(); ++i) { if (!i->second.piece->done && i->second.last_push_end < pull_to) { pull_to = i->second.last_push_end; @@ -697,6 +729,7 @@ Player::pass () pull_to = _silent.position(); } + LOG_DEBUG_PLAYER("Emitting audio up to %1", to_string(pull_to)); list, DCPTime> > audio = _audio_merger.pull (pull_to); for (list, DCPTime> >::iterator i = audio.begin(); i != audio.end(); ++i) { if (_last_audio_time && i->second < *_last_audio_time) { @@ -724,15 +757,6 @@ Player::pass () return done; } -list -Player::closed_captions_for_frame (DCPTime time) const -{ - boost::mutex::scoped_lock _lm (_mutex); - return _active_texts[TEXT_CLOSED_CAPTION].get ( - DCPTimePeriod(time, time + DCPTime::from_frames(1, _film->video_frame_rate())) - ); -} - /** @return Open subtitles for the frame at the given time, converted to images */ optional Player::open_subtitles_for_frame (DCPTime time) const @@ -740,14 +764,30 @@ Player::open_subtitles_for_frame (DCPTime time) const list captions; int const vfr = _film->video_frame_rate(); - BOOST_FOREACH ( - PlayerText j, + for ( + auto j: _active_texts[TEXT_OPEN_SUBTITLE].get_burnt(DCPTimePeriod(time, time + DCPTime::from_frames(1, vfr)), _always_burn_open_subtitles) ) { /* Bitmap subtitles */ - list c = transform_bitmap_texts (j.bitmap); - copy (c.begin(), c.end(), back_inserter (captions)); + for (auto i: j.bitmap) { + if (!i.image) { + continue; + } + + /* i.image will already have been scaled to fit _video_container_size */ + dcp::Size scaled_size (i.rectangle.width * _video_container_size.width, i.rectangle.height * _video_container_size.height); + + captions.push_back ( + PositionImage ( + i.image, + Position ( + lrint (_video_container_size.width * i.rectangle.x), + lrint (_video_container_size.height * i.rectangle.y) + ) + ) + ); + } /* String subtitles (rendered to an image) */ if (!j.string.empty ()) { @@ -771,13 +811,18 @@ Player::video (weak_ptr wp, ContentVideo video) return; } - FrameRateChange frc(piece->content->active_video_frame_rate(), _film->video_frame_rate()); + if (!piece->content->video->use()) { + return; + } + + FrameRateChange frc (_film, piece->content); if (frc.skip && (video.frame % 2) == 1) { return; } /* Time of the first frame we will emit */ DCPTime const time = content_video_to_dcp (piece, video.frame); + LOG_DEBUG_PLAYER("Received video frame %1 at %2", video.frame, to_string(time)); /* Discard if it's before the content's period or the last accurate seek. We can't discard if it's after the content's period here as in that case we still need to fill any gap between @@ -790,36 +835,50 @@ Player::video (weak_ptr wp, ContentVideo video) /* Fill gaps that we discover now that we have some video which needs to be emitted. This is where we need to fill to. */ - DCPTime fill_to = min (time, piece->content->end()); + DCPTime fill_to = min (time, piece->content->end(_film)); if (_last_video_time) { DCPTime fill_from = max (*_last_video_time, piece->content->position()); - LastVideoMap::const_iterator last = _last_video.find (wp); - if (_film->three_d()) { - DCPTime j = fill_from; - Eyes eyes = _last_video_eyes.get_value_or(EYES_LEFT); - if (eyes == EYES_BOTH) { - eyes = EYES_LEFT; - } - while (j < fill_to || eyes != video.eyes) { - if (last != _last_video.end()) { - shared_ptr copy = last->second->shallow_copy(); - copy->set_eyes (eyes); - emit_video (copy, j); - } else { - emit_video (black_player_video_frame(eyes), j); + + /* Fill if we have more than half a frame to do */ + if ((fill_to - fill_from) > one_video_frame() / 2) { + LastVideoMap::const_iterator last = _last_video.find (wp); + if (_film->three_d()) { + Eyes fill_to_eyes = video.eyes; + if (fill_to_eyes == EYES_BOTH) { + fill_to_eyes = EYES_LEFT; } - if (eyes == EYES_RIGHT) { - j += one_video_frame(); + if (fill_to == piece->content->end(_film)) { + /* Don't fill after the end of the content */ + fill_to_eyes = EYES_LEFT; } - eyes = increment_eyes (eyes); - } - } else { - for (DCPTime j = fill_from; j < fill_to; j += one_video_frame()) { - if (last != _last_video.end()) { - emit_video (last->second, j); - } else { - emit_video (black_player_video_frame(EYES_BOTH), j); + DCPTime j = fill_from; + Eyes eyes = _last_video_eyes.get_value_or(EYES_LEFT); + if (eyes == EYES_BOTH) { + eyes = EYES_LEFT; + } + while (j < fill_to || eyes != fill_to_eyes) { + if (last != _last_video.end()) { + LOG_DEBUG_PLAYER("Fill using last video at %1 in 3D mode", to_string(j)); + shared_ptr copy = last->second->shallow_copy(); + copy->set_eyes (eyes); + emit_video (copy, j); + } else { + LOG_DEBUG_PLAYER("Fill using black at %1 in 3D mode", to_string(j)); + emit_video (black_player_video_frame(eyes), j); + } + if (eyes == EYES_RIGHT) { + j += one_video_frame(); + } + eyes = increment_eyes (eyes); + } + } else { + for (DCPTime j = fill_from; j < fill_to; j += one_video_frame()) { + if (last != _last_video.end()) { + emit_video (last->second, j); + } else { + emit_video (black_player_video_frame(EYES_BOTH), j); + } } } } @@ -829,22 +888,22 @@ Player::video (weak_ptr wp, ContentVideo video) new PlayerVideo ( video.image, piece->content->video->crop (), - piece->content->video->fade (video.frame), - piece->content->video->scale().size ( - piece->content->video, _video_container_size, _film->frame_size () - ), + piece->content->video->fade (_film, video.frame), + scale_for_display(piece->content->video->scaled_size(_film->frame_size()), _video_container_size, _film->frame_size()), _video_container_size, video.eyes, video.part, piece->content->video->colour_conversion(), + piece->content->video->range(), piece->content, - video.frame + video.frame, + false ) ); DCPTime t = time; for (int i = 0; i < frc.repeat; ++i) { - if (t < piece->content->end()) { + if (t < piece->content->end(_film)) { emit_video (_last_video[wp], t); } t += one_video_frame (); @@ -864,10 +923,14 @@ Player::audio (weak_ptr wp, AudioStreamPtr stream, ContentAudio content_a shared_ptr content = piece->content->audio; DCPOMATIC_ASSERT (content); + int const rfr = content->resampled_frame_rate (_film); + /* Compute time in the DCP */ DCPTime time = resampled_audio_to_dcp (piece, content_audio.frame); + LOG_DEBUG_PLAYER("Received audio frame %1 at %2", content_audio.frame, to_string(time)); + /* And the end of this block in the DCP */ - DCPTime end = time + DCPTime::from_frames(content_audio.audio->frames(), content->resampled_frame_rate()); + DCPTime end = time + DCPTime::from_frames(content_audio.audio->frames(), rfr); /* Remove anything that comes before the start or after the end of the content */ if (time < piece->content->position()) { @@ -878,17 +941,15 @@ Player::audio (weak_ptr wp, AudioStreamPtr stream, ContentAudio content_a } content_audio.audio = cut.first; time = cut.second; - } else if (time > piece->content->end()) { + } else if (time > piece->content->end(_film)) { /* Discard it all */ return; - } else if (end > piece->content->end()) { - Frame const remaining_frames = DCPTime(piece->content->end() - time).frames_round(_film->audio_frame_rate()); + } else if (end > piece->content->end(_film)) { + Frame const remaining_frames = DCPTime(piece->content->end(_film) - time).frames_round(rfr); if (remaining_frames == 0) { return; } - shared_ptr cut (new AudioBuffers (content_audio.audio->channels(), remaining_frames)); - cut->copy_from (content_audio.audio.get(), remaining_frames, 0, 0); - content_audio.audio = cut; + content_audio.audio.reset (new AudioBuffers(content_audio.audio, remaining_frames, 0)); } DCPOMATIC_ASSERT (content_audio.audio->frames() > 0); @@ -940,10 +1001,20 @@ Player::bitmap_text_start (weak_ptr wp, weak_ptr wc, C subtitle.sub.rectangle.height *= text->y_scale (); PlayerText ps; - ps.bitmap.push_back (subtitle.sub); + shared_ptr image = subtitle.sub.image; + + /* We will scale the subtitle up to fit _video_container_size */ + int const width = subtitle.sub.rectangle.width * _video_container_size.width; + int const height = subtitle.sub.rectangle.height * _video_container_size.height; + if (width == 0 || height == 0) { + return; + } + + dcp::Size scaled_size (width, height); + ps.bitmap.push_back (BitmapText(image->scale(scaled_size, dcp::YUVToRGB::REC601, image->pixel_format(), true, _fast), subtitle.sub.rectangle)); DCPTime from (content_time_to_dcp (piece, subtitle.from())); - _active_texts[subtitle.type()].add_from (wc, ps, from); + _active_texts[text->type()].add_from (wc, ps, from); } void @@ -958,11 +1029,11 @@ Player::plain_text_start (weak_ptr wp, weak_ptr wc, Co PlayerText ps; DCPTime const from (content_time_to_dcp (piece, subtitle.from())); - if (from > piece->content->end()) { + if (from > piece->content->end(_film)) { return; } - BOOST_FOREACH (dcp::SubtitleString s, subtitle.subs) { + for (auto s: subtitle.subs) { s.set_h_position (s.h_position() + text->x_offset ()); s.set_v_position (s.v_position() + text->y_offset ()); float const xs = text->x_scale(); @@ -987,33 +1058,37 @@ Player::plain_text_start (weak_ptr wp, weak_ptr wc, Co ps.add_fonts (text->fonts ()); } - _active_texts[subtitle.type()].add_from (wc, ps, from); + _active_texts[text->type()].add_from (wc, ps, from); } void -Player::subtitle_stop (weak_ptr wp, weak_ptr wc, ContentTime to, TextType type) +Player::subtitle_stop (weak_ptr wp, weak_ptr wc, ContentTime to) { - if (!_active_texts[type].have (wc)) { + shared_ptr text = wc.lock (); + if (!text) { + return; + } + + if (!_active_texts[text->type()].have(wc)) { return; } shared_ptr piece = wp.lock (); - shared_ptr text = wc.lock (); - if (!piece || !text) { + if (!piece) { return; } DCPTime const dcp_to = content_time_to_dcp (piece, to); - if (dcp_to > piece->content->end()) { + if (dcp_to > piece->content->end(_film)) { return; } - pair from = _active_texts[type].add_to (wc, dcp_to); + pair from = _active_texts[text->type()].add_to (wc, dcp_to); - bool const always = (type == TEXT_OPEN_SUBTITLE && _always_burn_open_subtitles); + bool const always = (text->type() == TEXT_OPEN_SUBTITLE && _always_burn_open_subtitles); if (text->use() && !always && !text->burn()) { - Text (from.first, type, DCPTimePeriod (from.second, dcp_to)); + Text (from.first, text->type(), text->dcp_track().get_value_or(DCPTextTrack()), DCPTimePeriod (from.second, dcp_to)); } } @@ -1021,9 +1096,11 @@ void Player::seek (DCPTime time, bool accurate) { boost::mutex::scoped_lock lm (_mutex); + LOG_DEBUG_PLAYER("Seek to %1 (%2accurate)", to_string(time), accurate ? "" : "in"); - if (!_have_valid_pieces) { - setup_pieces (); + if (_suspended) { + /* We can't seek in this state */ + return; } if (_shuffler) { @@ -1041,12 +1118,16 @@ Player::seek (DCPTime time, bool accurate) _active_texts[i].clear (); } - BOOST_FOREACH (shared_ptr i, _pieces) { + for (auto i: _pieces) { if (time < i->content->position()) { - /* Before; seek to the start of the content */ - i->decoder->seek (dcp_to_content_time (i, i->content->position()), accurate); + /* Before; seek to the start of the content. Even if this request is for an inaccurate seek + we must seek this (following) content accurately, otherwise when we come to the end of the current + content we may not start right at the beginning of the next, causing a gap (if the next content has + been trimmed to a point between keyframes, or something). + */ + i->decoder->seek (dcp_to_content_time (i, i->content->position()), true); i->done = false; - } else if (i->content->position() <= time && time < i->content->end()) { + } else if (i->content->position() <= time && time < i->content->end(_film)) { /* During; seek to position */ i->decoder->seek (dcp_to_content_time (i, time), accurate); i->done = false; @@ -1115,13 +1196,13 @@ void Player::emit_audio (shared_ptr data, DCPTime time) { /* Log if the assert below is about to fail */ - if (_last_audio_time && time != *_last_audio_time) { + if (_last_audio_time && labs(time.get() - _last_audio_time->get()) > 1) { _film->log()->log(String::compose("Out-of-sequence emit %1 vs %2", to_string(time), to_string(*_last_audio_time)), LogEntry::TYPE_WARNING); } - /* This audio must follow on from the previous */ - DCPOMATIC_ASSERT (!_last_audio_time || time == *_last_audio_time); - Audio (data, time); + /* This audio must follow on from the previous, allowing for half a sample (at 48kHz) leeway */ + DCPOMATIC_ASSERT (!_last_audio_time || labs(time.get() - _last_audio_time->get()) < 2); + Audio (data, time, _film->audio_frame_rate()); _last_audio_time = time + DCPTime::from_frames (data->frames(), _film->audio_frame_rate()); } @@ -1162,43 +1243,57 @@ Player::discard_audio (shared_ptr audio, DCPTime time, DCPTi if (remaining_frames <= 0) { return make_pair(shared_ptr(), DCPTime()); } - shared_ptr cut (new AudioBuffers (audio->channels(), remaining_frames)); - cut->copy_from (audio.get(), remaining_frames, discard_frames, 0); + shared_ptr cut (new AudioBuffers(audio, remaining_frames, discard_frames)); return make_pair(cut, time + discard_time); } void Player::set_dcp_decode_reduction (optional reduction) { + Change (CHANGE_TYPE_PENDING, PlayerProperty::DCP_DECODE_REDUCTION, false); + { boost::mutex::scoped_lock lm (_mutex); if (reduction == _dcp_decode_reduction) { + lm.unlock (); + Change (CHANGE_TYPE_CANCELLED, PlayerProperty::DCP_DECODE_REDUCTION, false); return; } _dcp_decode_reduction = reduction; - _have_valid_pieces = false; + setup_pieces_unlocked (); } - Changed (PlayerProperty::DCP_DECODE_REDUCTION, false); + Change (CHANGE_TYPE_DONE, PlayerProperty::DCP_DECODE_REDUCTION, false); } -DCPTime +optional Player::content_time_to_dcp (shared_ptr content, ContentTime t) { boost::mutex::scoped_lock lm (_mutex); - if (_have_valid_pieces) { - setup_pieces (); - } - - BOOST_FOREACH (shared_ptr i, _pieces) { + for (auto i: _pieces) { if (i->content == content) { return content_time_to_dcp (i, t); } } - DCPOMATIC_ASSERT (false); - return DCPTime (); + /* We couldn't find this content; perhaps things are being changed over */ + return optional(); +} + + +shared_ptr +Player::playlist () const +{ + return _playlist ? _playlist : _film->playlist(); } + + +void +Player::atmos (weak_ptr, ContentAtmos data) +{ + Atmos (data.data, DCPTime::from_frames(data.frame, _film->video_frame_rate()), data.metadata); +} +