blob: b3d5834938efd21b54486aa823e5cc5079411089 [file] [log] [blame]
// Copyright 2018 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 "src/media/playback/mediaplayer/test/mediaplayer_test_util_view.h"
#include <fcntl.h>
#include <hid/usages.h>
#include <lib/ui/scenic/cpp/view_token_pair.h>
#include "lib/fidl/cpp/clone.h"
#include "lib/fidl/cpp/optional.h"
#include "lib/fsl/io/fd.h"
#include "lib/media/timeline/type_converters.h"
#include "src/lib/url/gurl.h"
#include "src/media/playback/mediaplayer/graph/formatting.h"
#include "src/media/playback/mediaplayer/test/mediaplayer_test_util_params.h"
namespace media_player {
namespace test {
namespace {
constexpr int32_t kDefaultWidth = 640;
constexpr int32_t kDefaultHeight = 100;
constexpr float kBackgroundElevation = 0.f;
constexpr float kVideoElevation = 1.0f;
constexpr float kProgressBarElevation = 1.0f;
constexpr float kProgressBarSliderElevation = 2.0f;
constexpr float kControlsGap = 12.0f;
constexpr float kControlsHeight = 36.0f;
// Determines whether the rectangle contains the point x,y.
bool Contains(const fuchsia::math::RectF& rect, float x, float y) {
return rect.x <= x && rect.y <= y && rect.x + rect.width >= x &&
rect.y + rect.height >= y;
}
int64_t rand_less_than(int64_t limit) {
return (static_cast<int64_t>(std::rand()) * RAND_MAX + std::rand()) % limit;
}
} // namespace
MediaPlayerTestUtilView::MediaPlayerTestUtilView(
scenic::ViewContext view_context, fit::function<void(int)> quit_callback,
const MediaPlayerTestUtilParams& params)
: scenic::BaseView(std::move(view_context), "Media Player"),
quit_callback_(std::move(quit_callback)),
params_(params),
background_node_(session()),
progress_bar_node_(session()),
progress_bar_slider_node_(session()) {
FXL_DCHECK(quit_callback_);
FXL_DCHECK(params_.is_valid());
FXL_DCHECK(!params_.urls().empty());
scenic::Material background_material(session());
background_material.SetColor(0x00, 0x00, 0x00, 0xff);
background_node_.SetMaterial(background_material);
root_node().AddChild(background_node_);
scenic::Material progress_bar_material(session());
progress_bar_material.SetColor(0x23, 0x23, 0x23, 0xff);
progress_bar_node_.SetMaterial(progress_bar_material);
root_node().AddChild(progress_bar_node_);
scenic::Material progress_bar_slider_material(session());
progress_bar_slider_material.SetColor(0x00, 0x00, 0xff, 0xff);
progress_bar_slider_node_.SetMaterial(progress_bar_slider_material);
root_node().AddChild(progress_bar_slider_node_);
// We start with a non-zero size so we get a progress bar regardless of
// whether we get video.
video_size_.width = 0;
video_size_.height = 0;
pixel_aspect_ratio_.width = 1;
pixel_aspect_ratio_.height = 1;
// Create a player from all that stuff.
player_ =
startup_context()
->ConnectToEnvironmentService<fuchsia::media::playback::Player>();
// Create the video view.
auto [view_token, view_holder_token] = scenic::ViewTokenPair::New();
player_->CreateView(std::move(view_token));
video_host_node_.reset(new scenic::EntityNode(session()));
video_view_holder_.reset(new scenic::ViewHolder(
session(), std::move(view_holder_token), "video view"));
video_host_node_->Attach(*video_view_holder_);
root_node().AddChild(*video_host_node_);
commands_.Init(player_.get());
player_.events().OnStatusChanged =
[this](fuchsia::media::playback::PlayerStatus status) {
HandleStatusChanged(status);
};
// Seed the random number generator.
std::srand(std::time(nullptr));
if (params_.experiment()) {
RunExperiment();
} else if (params_.test_seek()) {
TestSeek();
} else {
// Get the player primed now.
commands_.SetUrl(params_.urls().front());
commands_.Pause();
commands_.WaitForViewReady();
if (params_.auto_play()) {
commands_.Play();
}
ScheduleNextUrl();
}
commands_.Execute();
}
void MediaPlayerTestUtilView::RunExperiment() {
// Add experimental code here.
// In general, no implementation for this method should be submitted.
}
void MediaPlayerTestUtilView::TestSeek() {
commands_.SetUrl(params_.urls().front());
commands_.WaitForViewReady();
// Need to load content before deciding where to seek.
commands_.WaitForContentLoaded();
commands_.Invoke([this]() { ContinueTestSeek(); });
}
void MediaPlayerTestUtilView::ContinueTestSeek() {
if (duration_ns_ == 0) {
// We have no duration yet. Just start over at zero.
commands_.Seek(0);
commands_.Play();
commands_.WaitForEndOfStream();
commands_.Invoke([this]() { ContinueTestSeek(); });
FXL_LOG(INFO) << "Seek interval: beginning to end";
return;
}
// For the start position, generate a number in the range [0..duration_ns_]
// with a 10% chance of being zero.
int64_t seek_interval_start =
rand_less_than(duration_ns_ + duration_ns_ / 10);
if (seek_interval_start >= duration_ns_) {
seek_interval_start = 0;
}
// For the end position, choose a position between start and 10% past the
// duration. If this value is greater than the duration, the interval
// effectively ends at the end of the file.
int64_t seek_interval_end =
seek_interval_start +
rand_less_than((duration_ns_ + duration_ns_ / 10) - seek_interval_start);
commands_.Seek(seek_interval_start);
commands_.Play();
if (seek_interval_end >= duration_ns_) {
FXL_LOG(INFO) << "Seek interval: " << AsNs(seek_interval_start)
<< " to end";
commands_.WaitForEndOfStream();
} else {
FXL_LOG(INFO) << "Seek interval: " << AsNs(seek_interval_start) << " to "
<< AsNs(seek_interval_end);
commands_.WaitForSeekCompletion();
commands_.WaitForPosition(seek_interval_end);
}
commands_.Invoke([this]() { ContinueTestSeek(); });
}
void MediaPlayerTestUtilView::ScheduleNextUrl() {
if (++next_url_index_ == params_.urls().size()) {
if (!params_.loop()) {
// No more files, not looping.
return;
}
next_url_index_ = 0;
}
commands_.WaitForEndOfStream();
if (params_.urls().size() > 1) {
commands_.SetUrl(params_.urls()[next_url_index_]);
} else {
// Just one file...seek to the beginning.
commands_.Seek(0);
}
commands_.Play();
commands_.Invoke([this]() { ScheduleNextUrl(); });
}
MediaPlayerTestUtilView::~MediaPlayerTestUtilView() {}
void MediaPlayerTestUtilView::OnInputEvent(
fuchsia::ui::input::InputEvent event) {
if (event.is_pointer()) {
const auto& pointer = event.pointer();
if (pointer.phase == fuchsia::ui::input::PointerEventPhase::DOWN) {
if (duration_ns_ != 0 && Contains(controls_rect_, pointer.x, pointer.y)) {
// User poked the progress bar...seek.
player_->Seek((pointer.x - controls_rect_.x) * duration_ns_ /
controls_rect_.width);
if (state_ != State::kPlaying) {
player_->Play();
}
} else {
// User poked elsewhere.
TogglePlayPause();
}
}
} else if (event.is_keyboard()) {
auto& keyboard = event.keyboard();
if (keyboard.phase == fuchsia::ui::input::KeyboardEventPhase::PRESSED) {
switch (keyboard.hid_usage) {
case HID_USAGE_KEY_SPACE:
TogglePlayPause();
break;
case HID_USAGE_KEY_Q:
quit_callback_(0);
break;
default:
break;
}
}
}
}
void MediaPlayerTestUtilView::OnScenicEvent(fuchsia::ui::scenic::Event event) {
switch (event.Which()) {
case ::fuchsia::ui::scenic::Event::Tag::kGfx:
switch (event.gfx().Which()) {
case ::fuchsia::ui::gfx::Event::Tag::kViewConnected: {
auto& evt = event.gfx().view_connected();
OnChildAttached(evt.view_holder_id);
break;
}
case ::fuchsia::ui::gfx::Event::Tag::kViewDisconnected: {
auto& evt = event.gfx().view_disconnected();
OnChildUnavailable(evt.view_holder_id);
break;
}
default:
break;
}
default:
break;
}
}
void MediaPlayerTestUtilView::OnPropertiesChanged(
fuchsia::ui::gfx::ViewProperties old_properties) {
Layout();
}
void MediaPlayerTestUtilView::Layout() {
if (!has_logical_size())
return;
if (!scenic_ready_) {
scenic_ready_ = true;
commands_.NotifyViewReady();
}
// Make the background fill the space.
scenic::Rectangle background_shape(session(), logical_size().x,
logical_size().y);
background_node_.SetShape(background_shape);
background_node_.SetTranslation(
logical_size().x * .5f, logical_size().y * .5f, -kBackgroundElevation);
// Compute maximum size of video content after reserving space
// for decorations.
fuchsia::math::SizeF max_content_size;
max_content_size.width = logical_size().x;
max_content_size.height = logical_size().y - kControlsHeight - kControlsGap;
// Shrink video to fit if needed.
uint32_t video_width =
(video_size_.width == 0 ? kDefaultWidth : video_size_.width) *
pixel_aspect_ratio_.width;
uint32_t video_height =
(video_size_.height == 0 ? kDefaultHeight : video_size_.height) *
pixel_aspect_ratio_.height;
if (max_content_size.width * video_height <
max_content_size.height * video_width) {
content_rect_.width = max_content_size.width;
content_rect_.height = video_height * max_content_size.width / video_width;
} else {
content_rect_.width = video_width * max_content_size.height / video_height;
content_rect_.height = max_content_size.height;
}
// Position the video.
content_rect_.x = (logical_size().x - content_rect_.width) / 2.0f;
content_rect_.y = (logical_size().y - content_rect_.height - kControlsHeight -
kControlsGap) /
2.0f;
// Position the controls.
controls_rect_.x = content_rect_.x;
controls_rect_.y = content_rect_.y + content_rect_.height + kControlsGap;
controls_rect_.width = content_rect_.width;
controls_rect_.height = kControlsHeight;
// Put the progress bar under the content.
scenic::Rectangle progress_bar_shape(session(), controls_rect_.width,
controls_rect_.height);
progress_bar_node_.SetShape(progress_bar_shape);
progress_bar_node_.SetTranslation(
controls_rect_.x + controls_rect_.width * 0.5f,
controls_rect_.y + controls_rect_.height * 0.5f, -kProgressBarElevation);
// Put the progress bar slider on top of the progress bar.
scenic::Rectangle progress_bar_slider_shape(session(), controls_rect_.width,
controls_rect_.height);
progress_bar_slider_node_.SetShape(progress_bar_slider_shape);
progress_bar_slider_node_.SetTranslation(
controls_rect_.x + controls_rect_.width * 0.5f,
controls_rect_.y + controls_rect_.height * 0.5f,
-kProgressBarSliderElevation);
// Ask the view to fill the space.
video_view_holder_->SetViewProperties(0, 0, 0, content_rect_.width,
content_rect_.height, 1000.f, 0, 0, 0,
0, 0, 0);
InvalidateScene();
}
void MediaPlayerTestUtilView::OnSceneInvalidated(
fuchsia::images::PresentationInfo presentation_info) {
if (!has_physical_size())
return;
// Position the video.
if (video_host_node_) {
// TODO(dalesat): Fix this when SCN-1041 is fixed. Should be:
// video_host_node_->SetTranslation(
// content_rect_.x + content_rect_.width * 0.5f,
// content_rect_.y + content_rect_.height * 0.5f, kVideoElevation);
video_host_node_->SetTranslation(content_rect_.x, content_rect_.y,
-kVideoElevation);
}
float progress_bar_slider_width =
controls_rect_.width * normalized_progress();
scenic::Rectangle progress_bar_slider_shape(
session(), progress_bar_slider_width, controls_rect_.height);
progress_bar_slider_node_.SetShape(progress_bar_slider_shape);
progress_bar_slider_node_.SetTranslation(
controls_rect_.x + progress_bar_slider_width * 0.5f,
controls_rect_.y + controls_rect_.height * 0.5f,
-kProgressBarSliderElevation);
if (state_ == State::kPlaying) {
InvalidateScene();
}
}
void MediaPlayerTestUtilView::OnChildAttached(uint32_t view_holder_id) {
FXL_DCHECK(view_holder_id == video_view_holder_->id());
Layout();
}
void MediaPlayerTestUtilView::OnChildUnavailable(uint32_t view_holder_id) {
FXL_DCHECK(view_holder_id == video_view_holder_->id());
FXL_LOG(ERROR) << "Video view died unexpectedly";
video_host_node_->Detach();
video_host_node_.reset();
video_view_holder_.reset();
Layout();
}
void MediaPlayerTestUtilView::HandleStatusChanged(
const fuchsia::media::playback::PlayerStatus& status) {
// Process status received from the player.
if (status.timeline_function) {
timeline_function_ =
fidl::To<media::TimelineFunction>(*status.timeline_function);
state_ = status.end_of_stream
? State::kEnded
: (timeline_function_.subject_delta() == 0) ? State::kPaused
: State::kPlaying;
} else {
state_ = State::kPaused;
}
commands_.NotifyStatusChanged(status);
if (status.problem) {
if (!problem_shown_) {
FXL_LOG(ERROR) << "PROBLEM: " << status.problem->type << ", "
<< status.problem->details;
problem_shown_ = true;
}
} else {
problem_shown_ = false;
}
if (status.video_size && status.pixel_aspect_ratio &&
(!fidl::Equals(video_size_, *status.video_size) ||
!fidl::Equals(pixel_aspect_ratio_, *status.pixel_aspect_ratio))) {
video_size_ = *status.video_size;
pixel_aspect_ratio_ = *status.pixel_aspect_ratio;
Layout();
}
duration_ns_ = status.duration;
metadata_ = fidl::Clone(status.metadata);
InvalidateScene();
}
void MediaPlayerTestUtilView::TogglePlayPause() {
switch (state_) {
case State::kPaused:
player_->Play();
break;
case State::kPlaying:
player_->Pause();
break;
case State::kEnded:
player_->Seek(0);
player_->Play();
break;
default:
break;
}
}
int64_t MediaPlayerTestUtilView::progress_ns() const {
if (duration_ns_ == 0) {
return 0;
}
// Apply the timeline function to the current time.
int64_t position = timeline_function_(zx::clock::get_monotonic().get());
if (position < 0) {
position = 0;
}
if (position > duration_ns_) {
position = duration_ns_;
}
return position;
}
float MediaPlayerTestUtilView::normalized_progress() const {
if (duration_ns_ == 0) {
return 0.0f;
}
return progress_ns() / static_cast<float>(duration_ns_);
}
} // namespace test
} // namespace media_player