blob: 3f112b4ff3655d2aa35e5d06bf4573441de78e5b [file] [log] [blame]
// Copyright 2016 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "garnet/bin/media/media_service/media_player_impl.h"
#include "garnet/bin/media/fidl/fidl_formatting.h"
#include "garnet/bin/media/util/callback_joiner.h"
#include "lib/app/cpp/connect.h"
#include "lib/fxl/functional/make_copyable.h"
#include "lib/fxl/logging.h"
#include "lib/media/timeline/fidl_type_conversions.h"
#include "lib/media/timeline/timeline.h"
namespace media {
// static
std::shared_ptr<MediaPlayerImpl> MediaPlayerImpl::Create(
fidl::InterfaceHandle<SeekingReader> reader_handle,
fidl::InterfaceHandle<MediaRenderer> audio_renderer_handle,
fidl::InterfaceHandle<MediaRenderer> video_renderer_handle,
fidl::InterfaceRequest<MediaPlayer> request,
MediaServiceImpl* owner) {
return std::shared_ptr<MediaPlayerImpl>(new MediaPlayerImpl(
std::move(reader_handle), std::move(audio_renderer_handle),
std::move(video_renderer_handle), std::move(request), owner));
}
MediaPlayerImpl::MediaPlayerImpl(
fidl::InterfaceHandle<SeekingReader> reader_handle,
fidl::InterfaceHandle<MediaRenderer> audio_renderer_handle,
fidl::InterfaceHandle<MediaRenderer> video_renderer_handle,
fidl::InterfaceRequest<MediaPlayer> request,
MediaServiceImpl* owner)
: MediaServiceImpl::Product<MediaPlayer>(this, std::move(request), owner),
reader_handle_(std::move(reader_handle)) {
RCHECK(audio_renderer_handle || video_renderer_handle);
FXL_DCHECK(owner);
if (audio_renderer_handle) {
streams_by_medium_[MediaTypeMedium::AUDIO].renderer_handle_ =
std::move(audio_renderer_handle);
}
if (video_renderer_handle) {
streams_by_medium_[MediaTypeMedium::VIDEO].renderer_handle_ =
std::move(video_renderer_handle);
}
FLOG(log_channel_, BoundAs(FLOG_BINDING_KOID(binding())));
status_publisher_.SetCallbackRunner([this](const GetStatusCallback& callback,
uint64_t version) {
MediaPlayerStatusPtr status = MediaPlayerStatus::New();
status->timeline_transform = TimelineTransform::From(timeline_function_);
status->end_of_stream = end_of_stream_;
if (stream_types_) {
for (MediaTypePtr& stream_type : stream_types_) {
switch (stream_type->medium) {
case MediaTypeMedium::AUDIO:
status->content_has_audio = true;
break;
case MediaTypeMedium::VIDEO:
status->content_has_video = true;
break;
default:
break;
}
}
}
if (source_status_) {
status->audio_connected = source_status_->audio_connected;
status->video_connected = source_status_->video_connected;
status->metadata = source_status_->metadata.Clone();
if (source_status_->problem) {
status->problem = source_status_->problem.Clone();
} else if (state_ >= State::kFlushed && !status->audio_connected &&
!status->video_connected) {
// The source isn't reporting a problem, but neither audio nor video
// is connected. We report this as a problem so the client doesn't
// have to check these values separately.
status->problem = Problem::New();
status->problem->type = Problem::kProblemMediaTypeNotSupported;
}
}
callback(version, std::move(status));
});
state_ = State::kInactive;
media_service_ = owner->ConnectToEnvironmentService<MediaService>();
// Create a timeline controller.
media_service_->CreateTimelineController(timeline_controller_.NewRequest());
timeline_controller_->GetControlPoint(timeline_control_point_.NewRequest());
timeline_control_point_->GetTimelineConsumer(timeline_consumer_.NewRequest());
HandleTimelineControlPointStatusUpdates();
MaybeCreateSource();
}
MediaPlayerImpl::~MediaPlayerImpl() {}
void MediaPlayerImpl::MaybeCreateSource() {
if (!reader_handle_) {
return;
}
state_ = State::kWaiting;
media_service_->CreateSource(std::move(reader_handle_), nullptr,
source_.NewRequest());
FLOG(log_channel_, CreatedSource(FLOG_PTR_KOID(source_)));
HandleSourceStatusUpdates();
source_->Describe(
fxl::MakeCopyable([this](fidl::Array<MediaTypePtr> stream_types) mutable {
FLOG(log_channel_, ReceivedSourceDescription(stream_types.Clone()));
stream_types_ = std::move(stream_types);
ConnectSinks();
}));
}
void MediaPlayerImpl::ConnectSinks() {
std::shared_ptr<CallbackJoiner> callback_joiner = CallbackJoiner::Create();
size_t stream_index = 0;
for (MediaTypePtr& stream_type : stream_types_) {
auto iter = streams_by_medium_.find(stream_type->medium);
if (iter != streams_by_medium_.end()) {
if (iter->second.connected_) {
// TODO(dalesat): How do we choose the right stream?
FXL_DLOG(INFO) << "Stream " << stream_index
<< " redundant, already connected to sink with medium "
<< stream_type->medium;
++stream_index;
continue;
}
PrepareStream(&iter->second, stream_index, stream_type,
callback_joiner->NewCallback());
}
++stream_index;
}
callback_joiner->WhenJoined([this]() {
FLOG(log_channel_, StreamsPrepared());
state_ = State::kFlushed;
FLOG(log_channel_, Flushed());
Update();
});
}
void MediaPlayerImpl::PrepareStream(Stream* stream,
size_t index,
const MediaTypePtr& input_media_type,
const std::function<void()>& callback) {
FXL_DCHECK(media_service_);
if (!stream->sink_) {
FXL_DCHECK(stream->renderer_handle_);
media_service_->CreateSink(std::move(stream->renderer_handle_),
stream->sink_.NewRequest());
FLOG(log_channel_, CreatedSink(index, FLOG_PTR_KOID(stream->sink_)));
MediaTimelineControlPointPtr timeline_control_point;
stream->sink_->GetTimelineControlPoint(timeline_control_point.NewRequest());
timeline_controller_->AddControlPoint(std::move(timeline_control_point));
}
stream->sink_->ConsumeMediaType(
input_media_type.Clone(),
[this, stream, index,
callback](fidl::InterfaceHandle<MediaPacketConsumer> consumer) {
if (!consumer) {
// The sink couldn't build a conversion pipeline for the media type.
callback();
return;
}
stream->connected_ = true;
MediaPacketProducerPtr producer;
source_->GetPacketProducer(index, producer.NewRequest());
// Capture producer so it survives through the callback.
producer->Connect(consumer.Bind(), fxl::MakeCopyable([
this, callback, producer = std::move(producer)
]() { callback(); }));
});
}
void MediaPlayerImpl::Update() {
// This method is called whenever we might want to take action based on the
// current state and recent events. The current state is in |state_|. Recent
// events are recorded in |target_state_|, which indicates what state we'd
// like to transition to, |target_position_|, which can indicate a position
// we'd like to stream to, and |end_of_stream_| which tells us we've reached
// end of stream.
//
// Also relevant is |reader_transition_pending_|, which, when true, is treated
// pretty much like a |target_state_| of kFlushed. It indicates that we have
// a new reader we want to use, so the graph needs to be flushed and rebuilt.
// We use it instead of |target_state_| so that |target_state_| is preserved
// for when the new graph is built, at which point we'll work to transition
// to |target_state_|.
//
// The states are as follows:
//
// |kInactive|- Indicates that we have no reader.
// |kWaiting| - Indicates that we've done something asynchronous, and no
// further action should be taken by the state machine until that
// something completes (at which point the callback will change
// the state and call |Update|).
// |kFlushed| - Indicates that presentation time is not progressing and that
// the pipeline is not primed with packets. This is the initial
// state and the state we transition to in preparation for
// seeking. A seek is currently only done when when the pipeline
// is clear of packets.
// |kPrimed| - Indicates that presentation time is not progressing and that
// the pipeline is primed with packets. We transition to this
// state when the client calls |Pause|, either from |kFlushed| or
// |kPlaying| state.
// |kPlaying| - Indicates that presentation time is progressing and there are
// packets in the pipeline. We transition to this state when the
// client calls |Play|. If we're in |kFlushed| when |Play| is
// called, we transition through |kPrimed| state.
//
// The while loop that surrounds all the logic below is there because, after
// taking some action and transitioning to a new state, we may want to check
// to see if there's more to do in the new state. You'll also notice that
// the callback lambdas generally call |Update|.
while (true) {
switch (state_) {
case State::kInactive:
if (!reader_transition_pending_) {
return;
}
// Falls through.
case State::kFlushed:
// Presentation time is not progressing, and the pipeline is clear of
// packets.
if (reader_transition_pending_) {
// We need to switch to a new reader. Destroy the current source.
reader_transition_pending_ = false;
state_ = State::kInactive;
source_.Unbind();
stream_types_.reset();
source_status_.reset();
for (auto& pair : streams_by_medium_) {
pair.second.connected_ = false;
}
// The new source will start at position 0 unless a seek is requested.
// We set |program_range_min_pts_| and |transform_subject_time_| so
// the program range and timeline will be set properly.
// TODO(dalesat): Should |program_range_min_pts_| be kMinTime?
program_range_min_pts_ = 0;
transform_subject_time_ = 0;
status_publisher_.SendUpdates();
MaybeCreateSource();
return;
}
if (target_position_ != kUnspecifiedTime) {
// We want to seek. Enter |kWaiting| state until the operation is
// complete.
state_ = State::kWaiting;
// Capture the target position and clear it. If we get another seek
// request while setting the timeline transform and and seeking the
// source, we'll notice that and do those things again.
int64_t target_position = target_position_;
target_position_ = kUnspecifiedTime;
// |program_range_min_pts_| will be delivered in the |SetProgramRange|
// call, ensuring that the renderers discard packets with PTS values
// less than the target position. |transform_subject_time_| is used
// when setting the timeline.
transform_subject_time_ = target_position;
program_range_min_pts_ = target_position;
SetTimelineTransform(
0.0f, Timeline::local_now(),
[this, target_position](bool completed) {
if (target_position_ == target_position) {
// We've had a rendundant seek request. Ignore it.
target_position_ = kUnspecifiedTime;
} else if (target_position_ != kUnspecifiedTime) {
// We've had a seek request to a new position. Refrain from
// seeking the source and re-enter this sequence.
state_ = State::kFlushed;
FLOG(log_channel_, Flushed());
Update();
return;
}
// Seek to the new position.
FLOG(log_channel_, Seeking(target_position));
source_->Seek(target_position, [this]() {
state_ = State::kFlushed;
FLOG(log_channel_, Flushed());
// Back in |kFlushed|. Call |Update| to see
// if there's further action to be taken.
Update();
});
});
// Done for now. We're in kWaiting, and the callback will call Update
// when the Seek call is complete.
return;
}
if (target_state_ == State::kPlaying ||
target_state_ == State::kPrimed) {
// We want to transition to |kPrimed| or to |kPlaying|, for which
// |kPrimed| is a prerequisite. We enter |kWaiting| state, issue the
// |SetProgramRange| and |Prime| requests and transition to |kPrimed|
// when the operation is complete.
state_ = State::kWaiting;
timeline_control_point_->SetProgramRange(0, program_range_min_pts_,
kMaxTime);
FLOG(log_channel_, Priming());
timeline_control_point_->Prime([this]() {
state_ = State::kPrimed;
FLOG(log_channel_, Primed());
// Now we're in |kPrimed|. Call |Update| to see if there's further
// action to be taken.
Update();
});
// Done for now. We're in |kWaiting|, and the callback will call
// |Update| when the prime is complete.
return;
}
// No interesting events to respond to. Done for now.
return;
case State::kPrimed:
// Presentation time is not progressing, and the pipeline is primed with
// packets.
if (target_position_ != kUnspecifiedTime ||
target_state_ == State::kFlushed || reader_transition_pending_) {
// Either we want to seek or just want to transition to |kFlushed|,
// possibly because a reader transition is pending. We transition to
// |kWaiting|, issue the |Flush| request and transition to |kFlushed|
// when the operation is complete.
state_ = State::kWaiting;
FLOG(log_channel_, Flushing());
source_->Flush(
target_state_ != State::kFlushed && !reader_transition_pending_,
[this]() {
state_ = State::kFlushed;
FLOG(log_channel_, Flushed());
// Now we're in |kFlushed|. Call |Update| to see if there's
// further action to be taken.
Update();
});
// Done for now. We're in |kWaiting|, and the callback will call
// |Update| when the flush is complete.
return;
}
if (target_state_ == State::kPlaying) {
// We want to transition to |kPlaying|. Enter |kWaiting|, start the
// presentation timeline and transition to |kPlaying| when the
// operation completes.
state_ = State::kWaiting;
SetTimelineTransform(1.0f, Timeline::local_now() + kMinimumLeadTime,
[this](bool completed) {
state_ = State::kPlaying;
FLOG(log_channel_, Playing());
// Now we're in |kPlaying|. Call |Update| to
// see if there's further action to be taken.
Update();
});
// Done for now. We're in |kWaiting|, and the callback will call
// |Update| when the flush is complete.
return;
}
// No interesting events to respond to. Done for now.
return;
case State::kPlaying:
// Presentation time is progressing, and packets are moving through
// the pipeline.
if (target_position_ != kUnspecifiedTime ||
target_state_ == State::kFlushed ||
target_state_ == State::kPrimed || reader_transition_pending_) {
// Either we want to seek or we want to stop playback, possibly
// because a reader transition is pending. In either case, we need
// to enter |kWaiting|, stop the presentation timeline and transition
// to |kPrimed| when the operation completes.
state_ = State::kWaiting;
SetTimelineTransform(0.0f, Timeline::local_now() + kMinimumLeadTime,
[this](bool completed) {
state_ = State::kPrimed;
FLOG(log_channel_, Primed());
// Now we're in |kPrimed|. Call |Update| to see
// if there's further action to be taken.
Update();
});
// Done for now. We're in |kWaiting|, and the callback will call
// |Update| when the flush is complete.
return;
}
if (end_of_stream_) {
// We've reached end of stream. The presentation timeline stops by
// itself, so we just need to transition to |kPrimed|.
target_state_ = State::kPrimed;
state_ = State::kPrimed;
FLOG(log_channel_, EndOfStream());
// Loop around to check if there's more work to do.
break;
}
// No interesting events to respond to. Done for now.
return;
case State::kWaiting:
// Waiting for some async operation. Nothing to do until it completes.
return;
}
}
}
void MediaPlayerImpl::SetTimelineTransform(
float rate,
int64_t reference_time,
const TimelineConsumer::SetTimelineTransformCallback callback) {
TimelineTransformPtr timeline_transform =
CreateTimelineTransform(rate, reference_time);
FLOG(log_channel_, SettingTimelineTransform(timeline_transform.Clone()));
timeline_consumer_->SetTimelineTransform(std::move(timeline_transform),
callback);
}
TimelineTransformPtr MediaPlayerImpl::CreateTimelineTransform(
float rate,
int64_t reference_time) {
TimelineTransformPtr result = TimelineTransform::New();
result->reference_time = reference_time;
result->subject_time = transform_subject_time_;
TimelineRate timeline_rate(rate);
result->reference_delta = timeline_rate.reference_delta();
result->subject_delta = timeline_rate.subject_delta();
transform_subject_time_ = kUnspecifiedTime;
return result;
}
void MediaPlayerImpl::GetStatus(uint64_t version_last_seen,
const GetStatusCallback& callback) {
status_publisher_.Get(version_last_seen, callback);
}
void MediaPlayerImpl::Play() {
FLOG(log_channel_, PlayRequested());
target_state_ = State::kPlaying;
Update();
}
void MediaPlayerImpl::Pause() {
FLOG(log_channel_, PauseRequested());
target_state_ = State::kPrimed;
Update();
}
void MediaPlayerImpl::Seek(int64_t position) {
FLOG(log_channel_, SeekRequested(position));
target_position_ = position;
Update();
}
void MediaPlayerImpl::SetFileChannel(zx::channel file_channel) {
fidl::InterfaceHandle<SeekingReader> reader;
owner()->CreateFileChannelReader(std::move(file_channel),
reader.NewRequest());
SetReader(std::move(reader));
}
void MediaPlayerImpl::SetReader(
fidl::InterfaceHandle<SeekingReader> reader_handle) {
if (!reader_handle && !source_) {
// There was already no reader. Nothing to do.
return;
}
// Setting reader_transition_pending_ has a similar effect to setting
// target_state_ to State::kFlushed. We don't change target_state_ so the
// player will respect the client's desires once the reader transition is
// complete.
reader_transition_pending_ = true;
reader_handle_ = std::move(reader_handle);
// We clear |target_position_| so that a previously-requested seek that's
// still pending will not be applied to the new reader. The client can call
// |Seek| between this point and when the new graph is set up, and it will
// work.
target_position_ = kUnspecifiedTime;
Update();
}
void MediaPlayerImpl::HandleSourceStatusUpdates(uint64_t version,
MediaSourceStatusPtr status) {
if (status) {
source_status_ = std::move(status);
status_publisher_.SendUpdates();
}
source_->GetStatus(version,
[this](uint64_t version, MediaSourceStatusPtr status) {
HandleSourceStatusUpdates(version, std::move(status));
});
}
void MediaPlayerImpl::HandleTimelineControlPointStatusUpdates(
uint64_t version,
MediaTimelineControlPointStatusPtr status) {
if (status) {
timeline_function_ = status->timeline_transform.To<TimelineFunction>();
end_of_stream_ = status->end_of_stream;
status_publisher_.SendUpdates();
Update();
}
timeline_control_point_->GetStatus(
version,
[this](uint64_t version, MediaTimelineControlPointStatusPtr status) {
HandleTimelineControlPointStatusUpdates(version, std::move(status));
});
}
} // namespace media