| // |
| // Copyright 2020 Comcast Cable Communications Management, LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| // |
| // SPDX-License-Identifier: Apache-2.0// |
| // Copyright 2016 The Cobalt Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "third_party/starboard/rdk/shared/application_rdk.h" |
| |
| #include "starboard/common/log.h" |
| #include "starboard/event.h" |
| #include "starboard/speech_synthesis.h" |
| #include "starboard/shared/starboard/audio_sink/audio_sink_internal.h" |
| #include "starboard/shared/starboard/media/key_system_supportability_cache.h" |
| #include "starboard/shared/starboard/media/mime_supportability_cache.h" |
| #include "third_party/starboard/rdk/shared/window/window_internal.h" |
| #include "third_party/starboard/rdk/shared/log_override.h" |
| |
| #include <fcntl.h> |
| #include <poll.h> |
| #include <cstring> |
| #include <sys/eventfd.h> |
| #include <sys/timerfd.h> |
| #include <unistd.h> |
| #include <malloc.h> |
| |
| namespace third_party { |
| namespace starboard { |
| namespace rdk { |
| namespace shared { |
| |
| namespace libcobalt_api { |
| void Initialize(); |
| void Teardown(); |
| } |
| |
| namespace player { |
| void ForceStop(); |
| } // namespace player |
| |
| EssTerminateListener Application::terminateListener = { |
| //terminated |
| [](void* data) { reinterpret_cast<Application*>(data)->OnTerminated(); } |
| }; |
| |
| EssKeyListener Application::keyListener = { |
| // keyPressed |
| [](void* data, unsigned int key) { reinterpret_cast<Application*>(data)->OnKeyPressed(key); }, |
| // keyReleased |
| [](void* data, unsigned int key) { reinterpret_cast<Application*>(data)->OnKeyReleased(key); }, |
| // keyRepeat |
| [](void* data, unsigned int key) { reinterpret_cast<Application*>(data)->OnKeyPressed(key); } |
| }; |
| |
| EssSettingsListener Application::settingsListener = { |
| // displaySize |
| [](void *data, int width, int height ) { reinterpret_cast<Application*>(data)->OnDisplaySize(width, height); }, |
| // displaySafeArea |
| nullptr |
| }; |
| |
| const SbTime kEssRunLoopPeriod = 16666; // microseconds |
| |
| static void setTimerInterval(int fd, SbTime time) { |
| struct itimerspec timeout; |
| timeout.it_value.tv_sec = time / kSbTimeSecond; |
| timeout.it_value.tv_nsec = ( time % kSbTimeSecond ) * kSbTimeNanosecondsPerMicrosecond; |
| timeout.it_interval.tv_sec = timeout.it_value.tv_sec; |
| timeout.it_interval.tv_nsec = timeout.it_value.tv_nsec; |
| int rc = timerfd_settime(fd, 0, &timeout, NULL); |
| if (rc == -1) { |
| SB_LOG(ERROR) << "Failed to set timer interval, error: " << errno << " ("<< strerror(errno) << ')'; |
| } |
| } |
| |
| #if SB_API_VERSION >= 15 |
| Application::Application(SbEventHandleCallback sb_event_handle_callback) |
| : QueueApplication(sb_event_handle_callback) |
| #else |
| Application::Application() |
| : QueueApplication() |
| #endif |
| , input_handler_(new EssInput) |
| , hang_monitor_(new HangMonitor("Application")) { |
| essos_context_recycle_ = !!getenv("COBALT_ESSOS_CONTEXT_DESTROY"); |
| BuildEssosContext(); |
| } |
| |
| Application::~Application() { |
| EssContextDestroy(ctx_); |
| } |
| |
| void Application::Initialize() { |
| wakeup_fd_ = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); |
| if ( wakeup_fd_ == -1 ) { |
| SB_LOG(ERROR) << "Failed to create eventfd, error: " << errno << " (" << strerror(errno) << ')'; |
| } |
| |
| ess_timer_fd_ = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); |
| if ( ess_timer_fd_ == -1 ) { |
| SB_LOG(ERROR) << "Failed to create timerfd, error: " << errno << " (" << strerror(errno) << ')'; |
| } else { |
| setTimerInterval(ess_timer_fd_, kEssRunLoopPeriod); |
| } |
| |
| monitor_timer_fd_ = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); |
| if ( monitor_timer_fd_ == -1 ) { |
| SB_LOG(ERROR) << "Failed to create timerfd, error: " << errno << " (" << strerror(errno) << ')'; |
| hang_monitor_.reset(); |
| } else { |
| setTimerInterval(monitor_timer_fd_, hang_monitor_->GetResetInterval()); |
| } |
| |
| SbAudioSinkPrivate::Initialize(); |
| libcobalt_api::Initialize(); |
| using ::starboard::shared::starboard::media::KeySystemSupportabilityCache; |
| using ::starboard::shared::starboard::media::MimeSupportabilityCache; |
| MimeSupportabilityCache::GetInstance()->SetCacheEnabled(true); |
| KeySystemSupportabilityCache::GetInstance()->SetCacheEnabled(true); |
| |
| ScheduleMemoryUsageCheck(); |
| } |
| |
| void Application::Teardown() { |
| SbAudioSinkPrivate::TearDown(); |
| libcobalt_api::Teardown(); |
| TeardownJSONRPCLink(); |
| |
| close(ess_timer_fd_); |
| close(wakeup_fd_); |
| close(monitor_timer_fd_); |
| ess_timer_fd_ = wakeup_fd_ = monitor_timer_fd_ = -1; |
| } |
| |
| bool Application::MayHaveSystemEvents() { |
| return true; |
| } |
| |
| ::starboard::shared::starboard::Application::Event* |
| Application::PollNextSystemEvent() { |
| SbTime now = SbTimeGetMonotonicNow(); |
| if ((now - ess_loop_last_ts_) > kEssRunLoopPeriod) { |
| ess_loop_last_ts_ = now; |
| EssContextRunEventLoopOnce( ctx_ ); |
| } |
| return NULL; |
| } |
| |
| ::starboard::shared::starboard::Application::Event* |
| Application::WaitForSystemEventWithTimeout(SbTime time) { |
| struct timespec timeout; |
| struct pollfd fds[3]; |
| int fds_sz = 0; |
| int rc = 0; |
| |
| if ( !(ess_timer_fd_ < 0) ) { |
| fds[fds_sz].fd = ess_timer_fd_; |
| fds[fds_sz].events = POLLIN; |
| fds[fds_sz].revents = 0; |
| ++fds_sz; |
| } |
| |
| if ( !(wakeup_fd_ < 0) ) { |
| fds[fds_sz].fd = wakeup_fd_; |
| fds[fds_sz].events = POLLIN; |
| fds[fds_sz].revents = 0; |
| ++fds_sz; |
| } |
| |
| if ( !(monitor_timer_fd_ < 0) ) { |
| fds[fds_sz].fd = monitor_timer_fd_; |
| fds[fds_sz].events = POLLIN; |
| fds[fds_sz].revents = 0; |
| ++fds_sz; |
| } |
| |
| if ( fds_sz != 0 ) { |
| timeout.tv_sec = time / kSbTimeSecond; |
| timeout.tv_nsec = (time % kSbTimeSecond) * kSbTimeNanosecondsPerMicrosecond; |
| rc = ppoll(fds, fds_sz, &timeout, NULL); |
| } |
| |
| if ( rc > 0 ) { |
| for (int i = 0; i < fds_sz; ++i) { |
| if ( (fds[i].revents & POLLIN) != POLLIN ) |
| continue; |
| // Ack timer or wakeup event |
| uint64_t tmp; |
| read(fds[i].fd, &tmp, sizeof(uint64_t)); |
| |
| if ( fds[i].fd == monitor_timer_fd_ ) { |
| hang_monitor_->Reset(); |
| } |
| } |
| } |
| |
| return NULL; |
| } |
| |
| void Application::WakeSystemEventWait() { |
| uint64_t u = 1; |
| write(wakeup_fd_, &u, sizeof(uint64_t)); |
| } |
| |
| SbWindow Application::CreateSbWindow(const SbWindowOptions* options) { |
| SB_DCHECK(window_ == nullptr); |
| if (window_ != nullptr) |
| return kSbWindowInvalid; |
| MaterializeNativeWindow(); |
| window_ = new SbWindowPrivate(options); |
| return window_; |
| } |
| |
| bool Application::DestroySbWindow(SbWindow window) { |
| if (!SbWindowIsValid(window)) |
| return false; |
| window_ = nullptr; |
| delete window; |
| DestroyNativeWindow(); |
| return true; |
| } |
| |
| void Application::InjectInputEvent(SbInputData* data) { |
| if (native_window_ == 0) { |
| Application::DeleteDestructor<SbInputData>(data); |
| return; |
| } |
| |
| data->window = window_; |
| Inject(new Event(kSbEventTypeInput, data, |
| &Application::DeleteDestructor<SbInputData>)); |
| } |
| |
| void Application::Inject(Event* e) { |
| if (e && e->event && e->event->type == kSbEventTypeFreeze) { |
| player::ForceStop(); |
| } |
| |
| QueueApplication::Inject(e); |
| } |
| |
| void Application::OnSuspend() { |
| SbSpeechSynthesisCancel(); |
| DestroyNativeWindow(); |
| TeardownJSONRPCLink(); |
| setTimerInterval(ess_timer_fd_, kSbTimeSecond); |
| } |
| |
| void Application::OnResume() { |
| if ( essos_context_recycle_ ) |
| BuildEssosContext(); |
| |
| setTimerInterval(ess_timer_fd_, kEssRunLoopPeriod); |
| MaterializeNativeWindow(); |
| } |
| |
| void Application::OnTerminated() { |
| Stop(0); |
| } |
| |
| void Application::OnKeyPressed(unsigned int key) { |
| input_handler_->OnKeyPressed(key); |
| } |
| |
| void Application::OnKeyReleased(unsigned int key) { |
| input_handler_->OnKeyReleased(key); |
| } |
| |
| void Application::OnDisplaySize(int width, int height) { |
| if (window_width_ == width && window_height_ == height) { |
| resize_pending_ = false; |
| return; |
| } |
| |
| SB_DCHECK(native_window_ == 0); |
| resize_pending_ = true; |
| } |
| |
| void Application::MaterializeNativeWindow() { |
| if (native_window_ != 0) |
| return; |
| |
| bool error = false; |
| |
| if ( !EssContextGetDisplaySize(ctx_, &window_width_, &window_height_) ) { |
| error = true; |
| } |
| |
| if ( resize_pending_ ) { |
| EssContextResizeWindow(ctx_, window_width_, window_height_); |
| resize_pending_ = false; |
| } |
| |
| if ( !EssContextCreateNativeWindow(ctx_, window_width_, window_height_, &native_window_) ) { |
| error = true; |
| } |
| else if ( !EssContextStart(ctx_) ) { |
| error = true; |
| } |
| |
| if ( error ) { |
| const char *detail = EssContextGetLastErrorDetail(ctx_); |
| SB_LOG(ERROR) << "Essos error: '" << detail << '\''; |
| FatalError(); |
| } |
| } |
| |
| void Application::DestroyNativeWindow() { |
| if (native_window_ == 0) |
| return; |
| |
| if ( !EssContextDestroyNativeWindow(ctx_, native_window_) ) { |
| const char *detail = EssContextGetLastErrorDetail(ctx_); |
| SB_LOG(ERROR) << "Essos error: '" << detail << '\''; |
| } |
| |
| native_window_ = 0; |
| |
| if ( essos_context_recycle_ ) { |
| EssContextDestroy(ctx_); |
| ctx_ = NULL; |
| } |
| else |
| EssContextStop(ctx_); |
| } |
| |
| void Application::DisplayInfoChanged() { |
| if (state() != kStateStarted) |
| return; |
| |
| SbWindowSize window_size; |
| SbWindowGetSize(window_, &window_size); |
| auto *data = new SbEventWindowSizeChangedData(); |
| data->size = window_size; |
| data->window = window_; |
| WindowSizeChanged(data, &Application::DeleteDestructor<SbEventWindowSizeChangedData>); |
| } |
| |
| void Application::BuildEssosContext() { |
| bool error = false; |
| ctx_ = EssContextCreate(); |
| |
| if ( !EssContextInit(ctx_) ) { |
| error = true; |
| } |
| else if ( !EssContextSetTerminateListener(ctx_, this, &terminateListener) ) { |
| error = true; |
| } |
| else if ( !EssContextSetKeyListener(ctx_, this, &keyListener) ) { |
| error = true; |
| } |
| else if ( !EssContextSetSettingsListener(ctx_, this, &settingsListener) ) { |
| error = true; |
| } |
| |
| if ( error ) { |
| const char *detail = EssContextGetLastErrorDetail(ctx_); |
| SB_LOG(ERROR) << "Essos error: '" << detail << '\''; |
| FatalError(); |
| } |
| } |
| |
| void Application::FatalError() { |
| SB_LOG(ERROR) << "Exiting due to fatal error."; |
| ess_loop_last_ts_ = kSbTimeMax; // Stop Essos run loop |
| SbEventSchedule([](void* data) { |
| Application::Get()->Stop(0); |
| }, nullptr, 0); |
| } |
| |
| void Application::ReleaseMemory() { |
| Inject(new Event(kSbEventTypeLowMemory, NULL, [](void*) { |
| malloc_trim(0); |
| })); |
| } |
| |
| void Application::ScheduleMemoryUsageCheck(SbTime delay) { |
| SbEventSchedule([](void* data) { |
| SbTime back_off_timeout = Application::Get()->CheckMemoryUsage(); |
| if (back_off_timeout && back_off_timeout != kSbTimeMax) |
| Application::Get()->ScheduleMemoryUsageCheck(back_off_timeout); |
| }, nullptr, delay); |
| } |
| |
| SbTime Application::CheckMemoryUsage() { |
| static const int64_t kCPUMemoryPressureLimit = ([]() -> int64_t { |
| const char* env = std::getenv("COBALT_CPU_MEM_PRESSURE_IN_MB"); |
| int64_t limit_in_mb = SB_INT64_C(400); |
| if( env ) { |
| int64_t t = strtol(env, nullptr, 0); |
| if ( t >= 0 ) |
| limit_in_mb = t; |
| } |
| int64_t total_in_bytes = SbSystemGetTotalCPUMemory(); |
| return std::min(total_in_bytes, limit_in_mb * 1024 * 1024); |
| })(); |
| |
| if (!kCPUMemoryPressureLimit) |
| return kSbTimeMax; |
| |
| int64_t usage_in_bytes = SbSystemGetUsedCPUMemory(); |
| if (kCPUMemoryPressureLimit < usage_in_bytes) { |
| SB_LOG(INFO) << "Triggering memory pressure event. Current CPU mem. usage: " |
| << uint64_t(usage_in_bytes / 1024) << " kb, pressure limit: " |
| << uint64_t(kCPUMemoryPressureLimit / 1024) << " kb."; |
| ReleaseMemory(); |
| return 5 * kSbTimeSecond; |
| } |
| |
| return kSbTimeSecond; |
| } |
| |
| void Application::InjectAccessibilitySettingsChanged() { |
| Inject(new Event(kSbEventTypeAccessibilitySettingsChanged, NULL, NULL)); |
| } |
| |
| void Application::InjectAccessibilityCaptionSettingsChanged() { |
| Inject(new Event(kSbEventTypeAccessibilityCaptionSettingsChanged, NULL, NULL)); |
| } |
| |
| void Application::InjectAccessibilityTextToSpeechSettingsChanged() { |
| Inject(new Event(kSbEventTypeAccessibilityTextToSpeechSettingsChanged, NULL, NULL)); |
| } |
| |
| } // namespace shared |
| } // namespace rdk |
| } // namespace starboard |
| } // namespace third_party |