/*
- Copyright (C) 2013-2018 Carl Hetherington <cth@carlh.net>
+ Copyright (C) 2013-2020 Carl Hetherington <cth@carlh.net>
This file is part of DCP-o-matic.
*/
+#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"
#include "image_decoder.h"
#include "compose.hpp"
#include "shuffler.h"
+#include "timer.h"
#include <dcp/reel.h>
#include <dcp/reel_sound_asset.h>
#include <dcp/reel_subtitle_asset.h>
using boost::dynamic_pointer_cast;
using boost::optional;
using boost::scoped_ptr;
+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<const Film> film, shared_ptr<const Playlist> playlist)
+Player::Player (shared_ptr<const Film> film)
: _film (film)
- , _playlist (playlist)
- , _suspended (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)
+{
+ construct ();
+}
+
+Player::Player (shared_ptr<const Film> film, shared_ptr<const Playlist> 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));
+ _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_change (CHANGE_TYPE_DONE, Film::AUDIO_PROCESSOR);
setup_pieces_unlocked ();
}
+
bool
-have_video (shared_ptr<Piece> piece)
+have_video (shared_ptr<const Content> content)
{
- return piece->decoder && piece->decoder->video;
+ return static_cast<bool>(content->video) && content->video->use();
}
bool
-have_audio (shared_ptr<Piece> piece)
+have_audio (shared_ptr<const Content> content)
{
- return piece->decoder && piece->decoder->audio;
+ return static_cast<bool>(content->audio);
}
void
Player::setup_pieces_unlocked ()
{
+ _playback_length = _playlist ? _playlist->length(_film) : _film->length();
+
+ list<shared_ptr<Piece> > old_pieces = _pieces;
_pieces.clear ();
delete _shuffler;
_shuffler = new Shuffler();
_shuffler->Video.connect(bind(&Player::video, this, _1, _2));
- BOOST_FOREACH (shared_ptr<Content> i, _playlist->content ()) {
+ BOOST_FOREACH (shared_ptr<Content> i, playlist()->content()) {
if (!i->paths_valid ()) {
continue;
continue;
}
- shared_ptr<Decoder> decoder = decoder_factory (_film, i, _fast);
- FrameRateChange frc (_film, i);
-
- if (!decoder) {
- /* Not something that we can decode; e.g. Atmos content */
- continue;
+ shared_ptr<Decoder> old_decoder;
+ BOOST_FOREACH (shared_ptr<Piece> j, old_pieces) {
+ if (j->content == i) {
+ old_decoder = j->decoder;
+ break;
+ }
}
+ shared_ptr<Decoder> 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);
}
++j;
}
+
+ if (decoder->atmos) {
+ decoder->atmos->Data.connect (bind(&Player::atmos, this, weak_ptr<Piece>(piece), _1));
+ }
}
_stream_states.clear ();
}
}
- _black = Empty (_film, _pieces, bind(&have_video, _1));
- _silent = Empty (_film, _pieces, bind(&have_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;
Player::playlist_content_change (ChangeType type, int property, bool frequent)
{
if (type == CHANGE_TYPE_PENDING) {
- boost::mutex::scoped_lock lm (_mutex);
/* 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 = true;
+ ++_suspended;
} else if (type == CHANGE_TYPE_DONE) {
/* A change in our content has gone through. Re-build our pieces. */
setup_pieces ();
- _suspended = false;
+ --_suspended;
} else if (type == CHANGE_TYPE_CANCELLED) {
- boost::mutex::scoped_lock lm (_mutex);
- _suspended = false;
+ --_suspended;
}
Change (type, property, frequent);
eyes,
PART_WHOLE,
PresetColourConversion::all().front().conversion,
+ VIDEO_RANGE_FULL,
boost::weak_ptr<Content>(),
- boost::optional<Frame>()
+ boost::optional<Frame>(),
+ false
)
);
}
-Frame
-Player::dcp_to_content_video (shared_ptr<const Piece> piece, DCPTime t) const
-{
- DCPTime s = t - piece->content->position ();
- 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)
- then convert that ContentTime to frames at the content's rate. However this fails for
- situations like content at 29.9978733fps, DCP at 30fps. The accuracy of the Time type is not
- enough to distinguish between the two with low values of time (e.g. 3200 in Time units).
-
- Instead we convert the DCPTime using the DCP video rate then account for any skip/repeat.
- */
- return s.frames_floor (piece->frc.dcp) / piece->frc.factor ();
-}
-
-DCPTime
-Player::content_video_to_dcp (shared_ptr<const Piece> piece, Frame f) const
-{
- /* See comment in dcp_to_content_video */
- DCPTime const d = DCPTime::from_frames (f * piece->frc.factor(), piece->frc.dcp) - DCPTime(piece->content->trim_start(), piece->frc);
- return d + piece->content->position();
-}
-
-Frame
-Player::dcp_to_resampled_audio (shared_ptr<const Piece> piece, DCPTime t) const
-{
- DCPTime s = t - piece->content->position ();
- 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 ());
-}
DCPTime
Player::resampled_audio_to_dcp (shared_ptr<const Piece> piece, Frame f) const
{
- /* See comment in dcp_to_content_video */
+ /* See notes in content_video_to_dcp */
return DCPTime::from_frames (f, _film->audio_frame_rate())
- DCPTime (piece->content->trim_start(), piece->frc)
- + piece->content->position();
+ + piece->position();
}
ContentTime
Player::dcp_to_content_time (shared_ptr<const Piece> piece, DCPTime t) const
{
- DCPTime s = t - piece->content->position ();
+ DCPTime s = t - piece->position ();
s = min (piece->content->length_after_trim(_film), s);
return max (ContentTime (), ContentTime (s, piece->frc) + piece->content->trim_start());
}
-DCPTime
-Player::content_time_to_dcp (shared_ptr<const Piece> piece, ContentTime t) const
-{
- return max (DCPTime (), DCPTime (t - piece->content->trim_start(), piece->frc) + piece->content->position());
-}
-
list<shared_ptr<Font> >
Player::get_subtitle_fonts ()
{
maybe_add_asset (list<ReferencedReelAsset>& a, shared_ptr<dcp::ReelAsset> r, Frame reel_trim_start, Frame reel_trim_end, DCPTime from, int const ffr)
{
DCPOMATIC_ASSERT (r);
- r->set_entry_point (r->entry_point() + reel_trim_start);
- r->set_duration (r->duration() - reel_trim_start - reel_trim_end);
- if (r->duration() > 0) {
+ 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->duration(), ffr)))
+ ReferencedReelAsset(r, DCPTimePeriod(from, from + DCPTime::from_frames(r->actual_duration(), ffr)))
);
}
}
list<ReferencedReelAsset> a;
- BOOST_FOREACH (shared_ptr<Content> i, _playlist->content ()) {
+ BOOST_FOREACH (shared_ptr<Content> i, playlist()->content()) {
shared_ptr<DCPContent> j = dynamic_pointer_cast<DCPContent> (i);
if (!j) {
continue;
scoped_ptr<DCPDecoder> decoder;
try {
- decoder.reset (new DCPDecoder (_film, j, false));
+ decoder.reset (new DCPDecoder (_film, j, false, false, shared_ptr<DCPDecoder>()));
} catch (...) {
return a;
}
int64_t offset_from_end = 0;
BOOST_FOREACH (shared_ptr<dcp::Reel> k, decoder->reels()) {
/* Assume that main picture duration is the length of the reel */
- offset_from_end += k->main_picture()->duration();
+ offset_from_end += k->main_picture()->actual_duration();
}
BOOST_FOREACH (shared_ptr<dcp::Reel> k, decoder->reels()) {
/* Assume that main picture duration is the length of the reel */
- int64_t const reel_duration = k->main_picture()->duration();
+ int64_t const reel_duration = k->main_picture()->actual_duration();
/* See doc/design/trim_reels.svg */
- Frame const reel_trim_start = min(reel_duration, max(0LL, trim_start - offset_from_start));
- Frame const reel_trim_end = min(reel_duration, max(0LL, reel_duration - (offset_from_end - trim_end)));
+ 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 ()) {
return false;
}
- if (_playlist->length(_film) == 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;
}
continue;
}
- DCPTime const t = content_time_to_dcp (i, max(i->decoder->position(), i->content->trim_start()));
- if (t > i->content->end(_film)) {
+ DCPTime const t = i->content_time_to_dcp (max(i->decoder->position(), i->content->trim_start()));
+ if (t > i->end(_film)) {
i->done = true;
} else {
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:
+ {
earliest_content->done = earliest_content->decoder->pass ();
+ shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent>(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;
if (_last_audio_time) {
/* Sometimes the thing that happened last finishes fractionally before
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.
*/
- DCPOMATIC_ASSERT (labs(period.from.get() - _last_audio_time->get()) < 2);
+ 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()) {
/* 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<AudioStreamPtr, StreamState>::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;
return;
}
+ 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);
+ DCPTime const time = piece->content_video_to_dcp (video.frame);
/* 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
`now' and the end of the content's period.
*/
- if (time < piece->content->position() || (_last_video_time && time < *_last_video_time)) {
+ if (time < piece->position() || (_last_video_time && time < *_last_video_time)) {
return;
}
/* 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(_film));
+ DCPTime fill_to = min (time, piece->end(_film));
if (_last_video_time) {
- DCPTime fill_from = max (*_last_video_time, piece->content->position());
+ DCPTime fill_from = max (*_last_video_time, piece->position());
/* Fill if we have more than half a frame to do */
if ((fill_to - fill_from) > one_video_frame() / 2) {
if (fill_to_eyes == EYES_BOTH) {
fill_to_eyes = EYES_LEFT;
}
- if (fill_to == piece->content->end(_film)) {
+ if (fill_to == piece->end(_film)) {
/* Don't fill after the end of the content */
fill_to_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<PlayerVideo> 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) {
video.image,
piece->content->video->crop (),
piece->content->video->fade (_film, video.frame),
- piece->content->video->scale().size (
- piece->content->video, _video_container_size, _film->frame_size ()
- ),
+ 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(_film)) {
+ if (t < piece->end(_film)) {
emit_video (_last_video[wp], t);
}
t += one_video_frame ();
shared_ptr<AudioContent> 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);
/* And the end of this block in the DCP */
- DCPTime end = time + DCPTime::from_frames(content_audio.audio->frames(), content->resampled_frame_rate(_film));
+ 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()) {
- pair<shared_ptr<AudioBuffers>, DCPTime> cut = discard_audio (content_audio.audio, time, piece->content->position());
+ if (time < piece->position()) {
+ pair<shared_ptr<AudioBuffers>, DCPTime> cut = discard_audio (content_audio.audio, time, piece->position());
if (!cut.first) {
/* This audio is entirely discarded */
return;
}
content_audio.audio = cut.first;
time = cut.second;
- } else if (time > piece->content->end(_film)) {
+ } else if (time > piece->end(_film)) {
/* Discard it all */
return;
- } else if (end > piece->content->end(_film)) {
- Frame const remaining_frames = DCPTime(piece->content->end(_film) - time).frames_round(_film->audio_frame_rate());
+ } else if (end > piece->end(_film)) {
+ Frame const remaining_frames = DCPTime(piece->end(_film) - time).frames_round(rfr);
if (remaining_frames == 0) {
return;
}
- shared_ptr<AudioBuffers> 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);
PlayerText ps;
shared_ptr<Image> image = subtitle.sub.image;
+
/* We will scale the subtitle up to fit _video_container_size */
- dcp::Size scaled_size (subtitle.sub.rectangle.width * _video_container_size.width, subtitle.sub.rectangle.height * _video_container_size.height);
+ 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::YUV_TO_RGB_REC601, image->pixel_format(), true, _fast), subtitle.sub.rectangle));
- DCPTime from (content_time_to_dcp (piece, subtitle.from()));
+ DCPTime from (piece->content_time_to_dcp(subtitle.from()));
_active_texts[text->type()].add_from (wc, ps, from);
}
}
PlayerText ps;
- DCPTime const from (content_time_to_dcp (piece, subtitle.from()));
+ DCPTime const from (piece->content_time_to_dcp( subtitle.from()));
- if (from > piece->content->end(_film)) {
+ if (from > piece->end(_film)) {
return;
}
return;
}
- DCPTime const dcp_to = content_time_to_dcp (piece, to);
+ DCPTime const dcp_to = piece->content_time_to_dcp(to);
- if (dcp_to > piece->content->end(_film)) {
+ if (dcp_to > piece->end(_film)) {
return;
}
BOOST_FOREACH (shared_ptr<Piece> 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(_film)) {
+ } else if (i->content->position() <= time && time < i->end(_film)) {
/* During; seek to position */
i->decoder->seek (dcp_to_content_time (i, time), accurate);
i->done = false;
if (remaining_frames <= 0) {
return make_pair(shared_ptr<AudioBuffers>(), DCPTime());
}
- shared_ptr<AudioBuffers> cut (new AudioBuffers (audio->channels(), remaining_frames));
- cut->copy_from (audio.get(), remaining_frames, discard_frames, 0);
+ shared_ptr<AudioBuffers> cut (new AudioBuffers(audio, remaining_frames, discard_frames));
return make_pair(cut, time + discard_time);
}
BOOST_FOREACH (shared_ptr<Piece> i, _pieces) {
if (i->content == content) {
- return content_time_to_dcp (i, t);
+ return i->content_time_to_dcp (t);
}
}
/* We couldn't find this content; perhaps things are being changed over */
return optional<DCPTime>();
}
+
+
+shared_ptr<const Playlist>
+Player::playlist () const
+{
+ return _playlist ? _playlist : _film->playlist();
+}
+
+
+void
+Player::atmos (weak_ptr<Piece>, ContentAtmos data)
+{
+ Atmos (data.data, DCPTime::from_frames(data.frame, _film->video_frame_rate()), data.metadata);
+}
+