| // 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 |