| // 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 "topaz/examples/mediaplayer/mediaplayer_skia/mediaplayer_view.h" |
| |
| #include <fcntl.h> |
| #include <hid/usages.h> |
| |
| #include <iomanip> |
| |
| #include "lib/component/cpp/connect.h" |
| #include "lib/fidl/cpp/clone.h" |
| #include "lib/fidl/cpp/optional.h" |
| #include "lib/fsl/io/fd.h" |
| #include "lib/fxl/logging.h" |
| #include "lib/media/timeline/timeline.h" |
| #include "lib/media/timeline/type_converters.h" |
| #include "lib/url/gurl.h" |
| #include "third_party/skia/include/core/SkColor.h" |
| #include "third_party/skia/include/core/SkPath.h" |
| |
| namespace examples { |
| |
| namespace { |
| |
| constexpr uint32_t kVideoChildKey = 0u; |
| |
| constexpr int32_t kDefaultWidth = 640; |
| constexpr int32_t kDefaultHeight = 100; |
| |
| constexpr float kBackgroundElevation = 0.f; |
| constexpr float kVideoElevation = 1.f; |
| constexpr float kControlsElevation = 1.f; |
| |
| constexpr float kMargin = 4.0f; |
| constexpr float kControlsHeight = 36.0f; |
| constexpr float kSymbolWidth = 24.0f; |
| constexpr float kSymbolHeight = 24.0f; |
| constexpr float kSymbolPadding = 12.0f; |
| |
| constexpr SkColor kProgressBarForegroundColor = 0xff673ab7; // Deep Purple 500 |
| constexpr SkColor kProgressBarBackgroundColor = 0xffb39ddb; // Deep Purple 200 |
| constexpr SkColor kProgressBarSymbolColor = 0xffffffff; |
| |
| // 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; |
| } |
| |
| } // namespace |
| |
| MediaPlayerView::MediaPlayerView(scenic::ViewContext view_context, |
| async::Loop* loop, |
| const MediaPlayerParams& params) |
| : V1BaseView(std::move(view_context), "Media Player"), |
| |
| loop_(loop), |
| background_node_(session()), |
| controls_widget_(session()) { |
| FXL_DCHECK(loop); |
| FXL_DCHECK(params.is_valid()); |
| |
| scenic::Material background_material(session()); |
| background_material.SetColor(0x1a, 0x23, 0x7e, 0xff); // Indigo 900 |
| background_node_.SetMaterial(background_material); |
| parent_node().AddChild(background_node_); |
| |
| parent_node().AddChild(controls_widget_); |
| |
| // 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; |
| |
| player_ = startup_context() |
| ->ConnectToEnvironmentService<fuchsia::mediaplayer::Player>(); |
| player_.events().OnStatusChanged = |
| [this](fuchsia::mediaplayer::PlayerStatus status) { |
| HandleStatusChanged(status); |
| }; |
| |
| zx::eventpair video_view_owner_token, video_view_token; |
| if (zx::eventpair::create(0u, &video_view_owner_token, &video_view_token) != |
| ZX_OK) |
| FXL_NOTREACHED() << "failed to create tokens."; |
| player_->CreateView2(std::move(video_view_token)); |
| |
| zx::eventpair video_host_import_token; |
| video_host_node_.reset(new scenic::EntityNode(session())); |
| video_host_node_->ExportAsRequest(&video_host_import_token); |
| parent_node().AddChild(*video_host_node_); |
| GetViewContainer()->AddChild2(kVideoChildKey, |
| std::move(video_view_owner_token), |
| std::move(video_host_import_token)); |
| |
| if (!params.url().empty()) { |
| url::GURL url = url::GURL(params.url()); |
| |
| if (url.SchemeIsFile()) { |
| player_->SetFileSource(fsl::CloneChannelFromFileDescriptor( |
| fxl::UniqueFD(open(url.path().c_str(), O_RDONLY)).get())); |
| } else { |
| player_->SetHttpSource(params.url(), nullptr); |
| } |
| |
| // Get the first frames queued up so we can show something. |
| player_->Pause(); |
| } |
| |
| // These are for calculating frame rate. |
| frame_time_ = media::Timeline::local_now(); |
| prev_frame_time_ = frame_time_; |
| } |
| |
| MediaPlayerView::~MediaPlayerView() {} |
| |
| bool MediaPlayerView::OnInputEvent(fuchsia::ui::input::InputEvent event) { |
| bool handled = false; |
| if (event.is_pointer()) { |
| const auto& pointer = event.pointer(); |
| if (pointer.phase == fuchsia::ui::input::PointerEventPhase::DOWN) { |
| if (!Contains(progress_bar_rect_, pointer.x, pointer.y)) { |
| // User poked outside the progress bar. |
| TogglePlayPause(); |
| } else if (duration_ns_ != 0) { |
| // User poked the progress bar and we have duration...seek. |
| player_->Seek((pointer.x - progress_bar_rect_.x) * duration_ns_ / |
| progress_bar_rect_.width); |
| if (state_ != State::kPlaying) { |
| player_->Play(); |
| } |
| } |
| |
| handled = true; |
| } |
| } 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(); |
| handled = true; |
| break; |
| case HID_USAGE_KEY_Q: |
| loop_->Quit(); |
| handled = true; |
| break; |
| default: |
| break; |
| } |
| } |
| } |
| |
| return handled; |
| } |
| |
| void MediaPlayerView::OnPropertiesChanged( |
| fuchsia::ui::viewsv1::ViewProperties old_properties) { |
| Layout(); |
| } |
| |
| void MediaPlayerView::Layout() { |
| if (!has_logical_size()) |
| return; |
| |
| // Make the background fill the space. |
| scenic::Rectangle background_shape(session(), logical_size().width, |
| logical_size().height); |
| background_node_.SetShape(background_shape); |
| background_node_.SetTranslation(logical_size().width * .5f, |
| logical_size().height * .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().width - kMargin * 2; |
| max_content_size.height = |
| logical_size().height - kControlsHeight - kMargin * 3; |
| |
| // 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; |
| } |
| |
| // Add back in the decorations and center within view. |
| fuchsia::math::RectF ui_rect; |
| ui_rect.width = content_rect_.width; |
| ui_rect.height = content_rect_.height + kControlsHeight + kMargin; |
| ui_rect.x = (logical_size().width - ui_rect.width) / 2; |
| ui_rect.y = (logical_size().height - ui_rect.height) / 2; |
| |
| // Position the video. |
| content_rect_.x = ui_rect.x; |
| content_rect_.y = ui_rect.y; |
| |
| // Position the controls. |
| controls_rect_.x = content_rect_.x; |
| controls_rect_.y = content_rect_.y + content_rect_.height + kMargin; |
| controls_rect_.width = content_rect_.width; |
| controls_rect_.height = kControlsHeight; |
| |
| // Position the progress bar (for input). |
| progress_bar_rect_.x = controls_rect_.x + kSymbolWidth + kSymbolPadding * 2; |
| progress_bar_rect_.y = controls_rect_.y; |
| progress_bar_rect_.width = |
| controls_rect_.width - (kSymbolWidth + kSymbolPadding * 2); |
| progress_bar_rect_.height = controls_rect_.height; |
| |
| // Ask the view to fill the space. |
| fuchsia::ui::viewsv1::ViewProperties view_properties; |
| view_properties.view_layout = fuchsia::ui::viewsv1::ViewLayout::New(); |
| view_properties.view_layout->size.width = content_rect_.width; |
| view_properties.view_layout->size.height = content_rect_.height; |
| GetViewContainer()->SetChildProperties( |
| kVideoChildKey, fidl::MakeOptional(std::move(view_properties))); |
| |
| InvalidateScene(); |
| } |
| |
| void MediaPlayerView::OnSceneInvalidated( |
| fuchsia::images::PresentationInfo presentation_info) { |
| if (!has_physical_size()) |
| return; |
| |
| prev_frame_time_ = frame_time_; |
| frame_time_ = media::Timeline::local_now(); |
| |
| // Log the frame rate every five seconds. |
| if (state_ == State::kPlaying && |
| fxl::TimeDelta::FromNanoseconds(frame_time_).ToSeconds() / 5 != |
| fxl::TimeDelta::FromNanoseconds(prev_frame_time_).ToSeconds() / 5) { |
| FXL_DLOG(INFO) << "frame rate " << frame_rate() << " fps"; |
| } |
| |
| // Position the video. |
| if (video_host_node_) { |
| video_host_node_->SetTranslation(content_rect_.x, content_rect_.y, |
| kVideoElevation); |
| } |
| |
| // Draw the progress bar. |
| SkISize controls_size = |
| SkISize::Make(controls_rect_.width, controls_rect_.height); |
| SkCanvas* controls_canvas = controls_widget_.AcquireCanvas( |
| controls_rect_.width, controls_rect_.height, metrics().scale_x, |
| metrics().scale_y); |
| DrawControls(controls_canvas, controls_size); |
| controls_widget_.ReleaseAndSwapCanvas(); |
| controls_widget_.SetTranslation( |
| controls_rect_.x + controls_rect_.width * .5f, |
| controls_rect_.y + controls_rect_.height * .5f, kControlsElevation); |
| |
| // Animate the progress bar. |
| if (state_ == State::kPlaying) { |
| InvalidateScene(); |
| } |
| } |
| |
| void MediaPlayerView::OnChildAttached( |
| uint32_t child_key, fuchsia::ui::viewsv1::ViewInfo child_view_info) { |
| FXL_DCHECK(child_key == kVideoChildKey); |
| |
| parent_node().AddChild(*video_host_node_); |
| Layout(); |
| } |
| |
| void MediaPlayerView::OnChildUnavailable(uint32_t child_key) { |
| FXL_DCHECK(child_key == kVideoChildKey); |
| FXL_LOG(ERROR) << "Video view died unexpectedly"; |
| |
| video_host_node_->Detach(); |
| video_host_node_.reset(); |
| |
| GetViewContainer()->RemoveChild(child_key, nullptr); |
| Layout(); |
| } |
| |
| void MediaPlayerView::DrawControls(SkCanvas* canvas, const SkISize& size) { |
| canvas->clear(SK_ColorBLACK); |
| |
| // Draw the progress bar itself (blue on gray). |
| float progress_bar_left = kSymbolWidth + kSymbolPadding * 2; |
| float progress_bar_width = size.width() - progress_bar_left; |
| SkPaint paint; |
| paint.setColor(kProgressBarBackgroundColor); |
| canvas->drawRect( |
| SkRect::MakeXYWH(progress_bar_left, 0, progress_bar_width, size.height()), |
| paint); |
| |
| paint.setColor(kProgressBarForegroundColor); |
| canvas->drawRect( |
| SkRect::MakeXYWH(progress_bar_left, 0, progress_bar_width * progress(), |
| size.height()), |
| paint); |
| |
| paint.setColor(kProgressBarSymbolColor); |
| float symbol_left = kSymbolPadding; |
| float symbol_top = (size.height() - kSymbolHeight) / 2.0f; |
| if (state_ == State::kPlaying) { |
| // Playing...draw a pause symbol. |
| canvas->drawRect(SkRect::MakeXYWH(symbol_left, symbol_top, |
| kSymbolWidth / 3.0f, kSymbolHeight), |
| paint); |
| |
| canvas->drawRect( |
| SkRect::MakeXYWH(symbol_left + 2 * kSymbolWidth / 3.0f, symbol_top, |
| kSymbolWidth / 3.0f, kSymbolHeight), |
| paint); |
| } else { |
| // Playing...draw a play symbol. |
| SkPath path; |
| path.moveTo(symbol_left, symbol_top); |
| path.lineTo(symbol_left, symbol_top + kSymbolHeight); |
| path.lineTo(symbol_left + kSymbolWidth, symbol_top + kSymbolHeight / 2.0f); |
| path.lineTo(symbol_left, symbol_top); |
| canvas->drawPath(path, paint); |
| } |
| } |
| |
| void MediaPlayerView::HandleStatusChanged( |
| const fuchsia::mediaplayer::PlayerStatus& status) { |
| // Process status received from the player. |
| if (status.timeline_function) { |
| timeline_function_ = |
| fxl::To<media::TimelineFunction>(*status.timeline_function); |
| } |
| |
| previous_state_ = state_; |
| if (status.end_of_stream) { |
| state_ = State::kEnded; |
| } else if (timeline_function_.subject_delta() == 0) { |
| state_ = State::kPaused; |
| } else { |
| state_ = State::kPlaying; |
| } |
| |
| // TODO(dalesat): Display problems on the screen. |
| if (status.problem) { |
| if (!problem_shown_) { |
| FXL_DLOG(INFO) << "PROBLEM: " << status.problem->type << ", " |
| << status.problem->details; |
| problem_shown_ = true; |
| } |
| } else { |
| problem_shown_ = false; |
| } |
| |
| if (status.video_size && status.pixel_aspect_ratio && |
| (video_size_ != *status.video_size || |
| pixel_aspect_ratio_ != *status.pixel_aspect_ratio)) { |
| video_size_ = *status.video_size; |
| pixel_aspect_ratio_ = *status.pixel_aspect_ratio; |
| |
| FXL_LOG(INFO) << "video size " << status.video_size->width << "x" |
| << status.video_size->height << ", pixel aspect ratio " |
| << status.pixel_aspect_ratio->width << "x" |
| << status.pixel_aspect_ratio->height; |
| |
| Layout(); |
| } |
| |
| duration_ns_ = status.duration_ns; |
| |
| // TODO(dalesat): Display metadata on the screen. |
| if (status.metadata && !metadata_shown_) { |
| FXL_DLOG(INFO) << "duration " << std::fixed << std::setprecision(1) |
| << double(duration_ns_) / 1000000000.0 << " seconds"; |
| for (auto& property : status.metadata->properties) { |
| FXL_DLOG(INFO) << property.label << ": " << property.value; |
| } |
| |
| metadata_shown_ = true; |
| } |
| |
| InvalidateScene(); |
| } |
| |
| void MediaPlayerView::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; |
| } |
| } |
| |
| float MediaPlayerView::progress() const { |
| if (duration_ns_ == 0) { |
| return 0.0f; |
| } |
| |
| // Apply the timeline function to the current time. |
| int64_t position = timeline_function_(media::Timeline::local_now()); |
| |
| if (position < 0) { |
| position = 0; |
| } |
| |
| if (position > duration_ns_) { |
| position = duration_ns_; |
| } |
| |
| return position / static_cast<float>(duration_ns_); |
| } |
| |
| } // namespace examples |