| // Copyright 2017 Google Inc. 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 "starboard/shared/uwp/application_uwp.h" |
| |
| #include <WinSock2.h> |
| #include <mfapi.h> |
| #include <ppltasks.h> |
| #include <windows.h> |
| #include <D3D11.h> |
| |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #include "starboard/event.h" |
| #include "starboard/log.h" |
| #include "starboard/shared/starboard/application.h" |
| #include "starboard/shared/starboard/audio_sink/audio_sink_internal.h" |
| #include "starboard/shared/starboard/player/video_frame_internal.h" |
| #include "starboard/shared/uwp/window_internal.h" |
| #include "starboard/shared/win32/thread_private.h" |
| #include "starboard/shared/win32/wchar_utils.h" |
| #include "starboard/string.h" |
| #include "starboard/system.h" |
| |
| namespace sbwin32 = starboard::shared::win32; |
| |
| using Microsoft::WRL::ComPtr; |
| using starboard::shared::starboard::Application; |
| using starboard::shared::starboard::CommandLine; |
| using starboard::shared::uwp::ApplicationUwp; |
| using starboard::shared::uwp::GetArgvZero; |
| using starboard::shared::win32::stringToPlatformString; |
| using starboard::shared::win32::wchar_tToUTF8; |
| using starboard::shared::starboard::player::VideoFrame; |
| using Windows::ApplicationModel::Activation::ActivationKind; |
| using Windows::ApplicationModel::Activation::DialReceiverActivatedEventArgs; |
| using Windows::ApplicationModel::Activation::IActivatedEventArgs; |
| using Windows::ApplicationModel::Activation::IProtocolActivatedEventArgs; |
| using Windows::ApplicationModel::Core::CoreApplication; |
| using Windows::ApplicationModel::Core::CoreApplicationView; |
| using Windows::ApplicationModel::Core::IFrameworkView; |
| using Windows::ApplicationModel::Core::IFrameworkViewSource; |
| using Windows::ApplicationModel::SuspendingEventArgs; |
| using Windows::Foundation::EventHandler; |
| using Windows::Foundation::IAsyncOperation; |
| using Windows::Foundation::TimeSpan; |
| using Windows::Foundation::TypedEventHandler; |
| using Windows::Foundation::Uri; |
| using Windows::System::Threading::ThreadPoolTimer; |
| using Windows::System::Threading::TimerElapsedHandler; |
| using Windows::System::UserAuthenticationStatus; |
| using Windows::UI::Core::CoreDispatcherPriority; |
| using Windows::UI::Core::CoreProcessEventsOption; |
| using Windows::UI::Core::CoreWindow; |
| using Windows::UI::Core::DispatchedHandler; |
| using Windows::UI::Core::KeyEventArgs; |
| using Windows::UI::Popups::IUICommand; |
| using Windows::UI::Popups::MessageDialog; |
| using Windows::UI::Popups::UICommand; |
| using Windows::UI::Popups::UICommandInvokedHandler; |
| |
| namespace { |
| |
| const int kWinSockVersionMajor = 2; |
| const int kWinSockVersionMinor = 2; |
| |
| const char kDialParamPrefix[] = "cobalt-dial:?"; |
| |
| int main_return_value = 0; |
| |
| #if defined(ENABLE_DEBUG_COMMAND_LINE_SWITCHES) |
| |
| // Parses a starboard: URI scheme by splitting args at ';' boundaries. |
| std::vector<std::string> ParseStarboardUri(const std::string& uri) { |
| std::vector<std::string> result; |
| result.push_back(GetArgvZero()); |
| |
| size_t index = uri.find(':'); |
| if (index == std::string::npos) { |
| return result; |
| } |
| |
| std::string args = uri.substr(index + 1); |
| |
| while (!args.empty()) { |
| size_t next = args.find(';'); |
| result.push_back(args.substr(0, next)); |
| if (next == std::string::npos) { |
| return result; |
| } |
| args = args.substr(next + 1); |
| } |
| return result; |
| } |
| |
| #endif // defined(ENABLE_DEBUG_COMMAND_LINE_SWITCHES) |
| |
| std::unique_ptr<Application::Event> MakeDeepLinkEvent( |
| const std::string& uri_string) { |
| SB_LOG(INFO) << "Navigate to: [" << uri_string << "]"; |
| const size_t kMaxDeepLinkSize = 128 * 1024; |
| const std::size_t uri_size = uri_string.size(); |
| if (uri_size > kMaxDeepLinkSize) { |
| SB_NOTREACHED() << "App launch data too big: " << uri_size; |
| return nullptr; |
| } |
| |
| const int kBufferSize = static_cast<int>(uri_string.size()) + 1; |
| char* deep_link = new char[kBufferSize]; |
| SB_DCHECK(deep_link); |
| SbStringCopy(deep_link, uri_string.c_str(), kBufferSize); |
| |
| return std::unique_ptr<Application::Event>( |
| new Application::Event(kSbEventTypeLink, deep_link, |
| Application::DeleteArrayDestructor<const char*>)); |
| } |
| |
| // Returns if |full_string| ends with |substring|. |
| bool ends_with(const std::string& full_string, const std::string& substring) { |
| if (substring.length() > full_string.length()) { |
| return false; |
| } |
| return std::equal(substring.rbegin(), substring.rend(), full_string.rbegin()); |
| } |
| |
| } // namespace |
| |
| // Note that this is a "struct" and not a "class" because |
| // that's how it's defined in starboard/system.h |
| struct SbSystemPlatformErrorPrivate { |
| SbSystemPlatformErrorPrivate(const SbSystemPlatformErrorPrivate&) = delete; |
| SbSystemPlatformErrorPrivate& operator=( |
| const SbSystemPlatformErrorPrivate&) = delete; |
| |
| SbSystemPlatformErrorPrivate( |
| SbSystemPlatformErrorType type, |
| SbSystemPlatformErrorCallback callback, |
| void* user_data) |
| : callback_(callback), user_data_(user_data) { |
| SB_DCHECK(type == kSbSystemPlatformErrorTypeConnectionError); |
| |
| ApplicationUwp* app = ApplicationUwp::Get(); |
| app->RunInMainThreadAsync([this, callback, user_data, app]() { |
| MessageDialog^ dialog = ref new MessageDialog( |
| app->GetString("UNABLE_TO_CONTACT_YOUTUBE_1", |
| "Sorry, could not connect to YouTube.")); |
| dialog->Commands->Append( |
| MakeUICommand( |
| "OFFLINE_MESSAGE_TRY_AGAIN", "Try again", |
| kSbSystemPlatformErrorResponsePositive)); |
| dialog->Commands->Append( |
| MakeUICommand( |
| "EXIT_BUTTON", "Exit", |
| kSbSystemPlatformErrorResponseCancel)); |
| dialog->DefaultCommandIndex = 0; |
| dialog->CancelCommandIndex = 1; |
| IAsyncOperation<IUICommand^>^ operation = dialog->ShowAsync(); |
| dialog_operation_ = operation; |
| concurrency::create_task(operation).then([this](IUICommand^ command) { |
| delete this; |
| }); |
| }); |
| } |
| |
| UICommand^ MakeUICommand( |
| const char* id, |
| const char* fallback, |
| SbSystemPlatformErrorResponse response) { |
| ApplicationUwp* app = ApplicationUwp::Get(); |
| Platform::String^ label = app->GetString(id, fallback); |
| |
| return ref new UICommand(label, |
| ref new UICommandInvokedHandler( |
| [this, response](IUICommand^ command) { |
| callback_(response, user_data_); |
| })); |
| } |
| |
| void Clear() { |
| ApplicationUwp::Get()->RunInMainThreadAsync([this]() { |
| dialog_operation_->Cancel(); |
| }); |
| } |
| |
| private: |
| SbSystemPlatformErrorCallback callback_; |
| void* user_data_; |
| Platform::Agile<IAsyncOperation<IUICommand^>> dialog_operation_; |
| }; |
| |
| ref class App sealed : public IFrameworkView { |
| public: |
| App() : previously_activated_(false) {} |
| |
| // IFrameworkView methods. |
| virtual void Initialize(CoreApplicationView^ applicationView) { |
| SbAudioSinkPrivate::Initialize(); |
| CoreApplication::Suspending += |
| ref new EventHandler<SuspendingEventArgs^>(this, &App::OnSuspending); |
| CoreApplication::Resuming += |
| ref new EventHandler<Object^>(this, &App::OnResuming); |
| applicationView->Activated += |
| ref new TypedEventHandler<CoreApplicationView^, IActivatedEventArgs^>( |
| this, &App::OnActivated); |
| } |
| virtual void SetWindow(CoreWindow^ window) { |
| ApplicationUwp::Get()->SetCoreWindow(window); |
| window->KeyUp += ref new TypedEventHandler<CoreWindow^, KeyEventArgs^>( |
| this, &App::OnKeyUp); |
| window->KeyDown += ref new TypedEventHandler<CoreWindow^, KeyEventArgs^>( |
| this, &App::OnKeyDown); |
| } |
| virtual void Load(Platform::String^ entryPoint) {} |
| virtual void Run() { |
| main_return_value = application_.Run( |
| static_cast<int>(argv_.size()), const_cast<char**>(argv_.data())); |
| } |
| virtual void Uninitialize() { SbAudioSinkPrivate::TearDown(); } |
| |
| void OnSuspending(Platform::Object^ sender, SuspendingEventArgs^ args) { |
| SB_DLOG(INFO) << "Suspending"; |
| // Note if we dispatch "suspend" here before pause, application.cc |
| // will inject the "pause" which will cause us to go async which |
| // will cause us to not have completed the suspend operation before |
| // returning, which UWP requires. |
| ApplicationUwp::Get()->DispatchAndDelete( |
| new ApplicationUwp::Event(kSbEventTypePause, NULL, NULL)); |
| ApplicationUwp::Get()->DispatchAndDelete( |
| new ApplicationUwp::Event(kSbEventTypeSuspend, NULL, NULL)); |
| } |
| |
| void OnResuming(Platform::Object^ sender, Platform::Object^ args) { |
| SB_DLOG(INFO) << "Resuming"; |
| ApplicationUwp::Get()->DispatchAndDelete( |
| new ApplicationUwp::Event(kSbEventTypeResume, NULL, NULL)); |
| ApplicationUwp::Get()->DispatchAndDelete( |
| new ApplicationUwp::Event(kSbEventTypeUnpause, NULL, NULL)); |
| } |
| |
| void OnKeyUp(CoreWindow^ sender, KeyEventArgs^ args) { |
| ApplicationUwp::Get()->OnKeyEvent(sender, args, true); |
| } |
| |
| void OnKeyDown(CoreWindow^ sender, KeyEventArgs^ args) { |
| ApplicationUwp::Get()->OnKeyEvent(sender, args, false); |
| } |
| |
| void OnActivated( |
| CoreApplicationView^ applicationView, IActivatedEventArgs^ args) { |
| bool command_line_set = false; |
| |
| // Please see application lifecyle description: |
| // https://docs.microsoft.com/en-us/windows/uwp/launch-resume/app-lifecycle |
| // Note that this document was written for Xaml apps not core apps, |
| // so for us the precise API is a little different. |
| // The substance is that, while OnActiviated is definitely called the |
| // first time the application is started, it may additionally called |
| // in other cases while the process is already running. Starboard |
| // applications cannot fully restart in a process lifecycle, |
| // so we interpret the first activation and the subsequent ones differently. |
| if (args->Kind == ActivationKind::Protocol) { |
| Uri^ uri = dynamic_cast<IProtocolActivatedEventArgs^>(args)->Uri; |
| |
| #if defined(ENABLE_DEBUG_COMMAND_LINE_SWITCHES) |
| // The starboard: scheme provides commandline arguments, but that's |
| // only allowed during a process's first activation. |
| std::string scheme = sbwin32::platformStringToString(uri->SchemeName); |
| |
| if (!previously_activated_ && ends_with(scheme, "-starboard")) { |
| std::string uri_string = wchar_tToUTF8(uri->RawUri->Data()); |
| // args_ is a vector of std::string, but argv_ is a vector of |
| // char* into args_ so as to compose a char**. |
| args_ = ParseStarboardUri(uri_string); |
| for (const std::string& arg : args_) { |
| argv_.push_back(arg.c_str()); |
| } |
| |
| ApplicationUwp::Get()->SetCommandLine( |
| static_cast<int>(argv_.size()), argv_.data()); |
| command_line_set = true; |
| } |
| #endif // defined(ENABLE_DEBUG_COMMAND_LINE_SWITCHES) |
| if (uri->SchemeName->Equals("youtube") || |
| uri->SchemeName->Equals("ms-xbl-07459769")) { |
| std::string uri_string = sbwin32::platformStringToString(uri->RawUri); |
| |
| // Strip the protocol from the uri. |
| size_t index = uri_string.find(':'); |
| if (index != std::string::npos) { |
| uri_string = uri_string.substr(index + 1); |
| } |
| |
| ProcessDeepLinkUri(&uri_string); |
| } |
| } else if (args->Kind == ActivationKind::DialReceiver) { |
| DialReceiverActivatedEventArgs^ dial_args = |
| dynamic_cast<DialReceiverActivatedEventArgs^>(args); |
| SB_CHECK(dial_args); |
| Platform::String^ arguments = dial_args->Arguments; |
| if (previously_activated_) { |
| std::string uri_string = |
| kDialParamPrefix + sbwin32::platformStringToString(arguments); |
| ProcessDeepLinkUri(&uri_string); |
| } else { |
| const char kYouTubeTVurl[] = "--url=https://www.youtube.com/tv?"; |
| std::string activation_args = |
| kYouTubeTVurl + sbwin32::platformStringToString(arguments); |
| SB_DLOG(INFO) << "Dial Activation url: " << activation_args; |
| args_.push_back(GetArgvZero()); |
| args_.push_back(activation_args); |
| argv_.push_back(args_.front().c_str()); |
| argv_.push_back(args_.back().c_str()); |
| ApplicationUwp::Get()->SetCommandLine(static_cast<int>(argv_.size()), |
| argv_.data()); |
| command_line_set = true; |
| } |
| } |
| previous_activation_kind_ = args->Kind; |
| |
| if (!previously_activated_) { |
| if (!command_line_set) { |
| args_.push_back(GetArgvZero()); |
| argv_.push_back(args_.begin()->c_str()); |
| ApplicationUwp::Get()->SetCommandLine( |
| static_cast<int>(argv_.size()), argv_.data()); |
| } |
| CoreWindow::GetForCurrentThread()->Activate(); |
| // Call DispatchStart async so the UWP system thinks we're activated. |
| // Some tools seem to want the application to be activated before |
| // interacting with them, some things are disallowed during activation |
| // (such as exiting), and DispatchStart (for example) runs |
| // automated tests synchronously. |
| ApplicationUwp::Get()->RunInMainThreadAsync([this]() { |
| ApplicationUwp::Get()->DispatchStart(); |
| }); |
| } |
| previously_activated_ = true; |
| } |
| private: |
| void ProcessDeepLinkUri(std::string *uri_string) { |
| SB_DCHECK(uri_string); |
| if (previously_activated_) { |
| std::unique_ptr<Application::Event> event = |
| MakeDeepLinkEvent(*uri_string); |
| SB_DCHECK(event); |
| ApplicationUwp::Get()->Inject(event.release()); |
| } else { |
| SB_DCHECK(!uri_string->empty()); |
| ApplicationUwp::Get()->SetStartLink(uri_string->c_str()); |
| } |
| } |
| |
| bool previously_activated_; |
| // Only valid if previously_activated_ is true |
| ActivationKind previous_activation_kind_; |
| std::vector<std::string> args_; |
| std::vector<const char *> argv_; |
| |
| starboard::shared::uwp::ApplicationUwp application_; |
| }; |
| |
| ref class Direct3DApplicationSource sealed : IFrameworkViewSource { |
| public: |
| Direct3DApplicationSource() {} |
| virtual IFrameworkView^ CreateView() { |
| return ref new App(); |
| } |
| }; |
| |
| namespace starboard { |
| namespace shared { |
| namespace uwp { |
| |
| // If an argv[0] is required, fill it in with the result of |
| // GetModuleFileName() |
| std::string GetArgvZero() { |
| const size_t kMaxModuleNameSize = 256; |
| wchar_t buffer[kMaxModuleNameSize]; |
| DWORD result = GetModuleFileName(NULL, buffer, kMaxModuleNameSize); |
| std::string arg; |
| if (result == 0) { |
| arg = "unknown"; |
| } else { |
| arg = wchar_tToUTF8(buffer, result).c_str(); |
| } |
| return arg; |
| } |
| |
| ApplicationUwp::ApplicationUwp() |
| : window_(kSbWindowInvalid), localized_strings_(SbSystemGetLocaleId()) {} |
| |
| ApplicationUwp::~ApplicationUwp() {} |
| |
| void ApplicationUwp::Initialize() {} |
| |
| void ApplicationUwp::Teardown() {} |
| |
| Application::Event* ApplicationUwp::GetNextEvent() { |
| SB_NOTREACHED(); |
| return nullptr; |
| } |
| |
| SbWindow ApplicationUwp::CreateWindowForUWP(const SbWindowOptions* options) { |
| // TODO: Determine why SB_DCHECK(IsCurrentThread()) fails in nplb, fix it, |
| // and add back this check. |
| |
| if (SbWindowIsValid(window_)) { |
| return kSbWindowInvalid; |
| } |
| |
| window_ = new SbWindowPrivate(options); |
| return window_; |
| } |
| |
| bool ApplicationUwp::DestroyWindow(SbWindow window) { |
| // TODO: Determine why SB_DCHECK(IsCurrentThread()) fails in nplb, fix it, |
| // and add back this check. |
| |
| if (!SbWindowIsValid(window)) { |
| SB_DLOG(ERROR) << __FUNCTION__ << ": Invalid context."; |
| return false; |
| } |
| |
| SB_DCHECK(window_ == window); |
| delete window; |
| window_ = kSbWindowInvalid; |
| |
| return true; |
| } |
| |
| bool ApplicationUwp::DispatchNextEvent() { |
| core_window_->Activate(); |
| core_window_->Dispatcher->ProcessEvents( |
| CoreProcessEventsOption::ProcessUntilQuit); |
| return false; |
| } |
| |
| void ApplicationUwp::Inject(Application::Event* event) { |
| RunInMainThreadAsync([this, event]() { |
| bool result = DispatchAndDelete(event); |
| if (!result) { |
| CoreApplication::Exit(); |
| } |
| }); |
| } |
| |
| void ApplicationUwp::InjectTimedEvent(Application::TimedEvent* timed_event) { |
| SbTimeMonotonic delay_usec = |
| timed_event->target_time - SbTimeGetMonotonicNow(); |
| if (delay_usec < 0) { |
| delay_usec = 0; |
| } |
| |
| // TimeSpan ticks are, like FILETIME, 100ns |
| const SbTimeMonotonic kTicksPerUsec = 10; |
| |
| TimeSpan timespan; |
| timespan.Duration = delay_usec * kTicksPerUsec; |
| |
| ScopedLock lock(mutex_); |
| ThreadPoolTimer^ timer = ThreadPoolTimer::CreateTimer( |
| ref new TimerElapsedHandler([this, timed_event](ThreadPoolTimer^ timer) { |
| RunInMainThreadAsync([this, timed_event]() { |
| // Even if the event is canceled, the callback can still fire. |
| // Thus, the existence of event in timer_event_map_ is used |
| // as a source of truth. |
| std::size_t number_erased = 0; |
| { |
| ScopedLock lock(mutex_); |
| number_erased = timer_event_map_.erase(timed_event->id); |
| } |
| if (number_erased > 0) { |
| timed_event->callback(timed_event->context); |
| } |
| }); |
| }), timespan); |
| timer_event_map_.emplace(timed_event->id, timer); |
| } |
| |
| void ApplicationUwp::CancelTimedEvent(SbEventId event_id) { |
| ScopedLock lock(mutex_); |
| auto it = timer_event_map_.find(event_id); |
| if (it == timer_event_map_.end()) { |
| return; |
| } |
| it->second->Cancel(); |
| timer_event_map_.erase(it); |
| } |
| |
| Application::TimedEvent* ApplicationUwp::GetNextDueTimedEvent() { |
| SB_NOTIMPLEMENTED(); |
| return nullptr; |
| } |
| |
| SbTimeMonotonic ApplicationUwp::GetNextTimedEventTargetTime() { |
| SB_NOTIMPLEMENTED(); |
| return 0; |
| } |
| SbSystemPlatformError ApplicationUwp::OnSbSystemRaisePlatformError( |
| SbSystemPlatformErrorType type, |
| SbSystemPlatformErrorCallback callback, |
| void* user_data) { |
| return new SbSystemPlatformErrorPrivate(type, callback, user_data); |
| } |
| |
| void ApplicationUwp::OnSbSystemClearPlatformError( |
| SbSystemPlatformError handle) { |
| if (handle == kSbSystemPlatformErrorInvalid) { |
| return; |
| } |
| static_cast<SbSystemPlatformErrorPrivate*>(handle)->Clear(); |
| } |
| |
| Platform::String^ ApplicationUwp::GetString( |
| const char* id, const char* fallback) const { |
| return stringToPlatformString(localized_strings_.GetString(id, fallback)); |
| } |
| |
| void ApplicationUwp::AcceptFrame(SbPlayer player, |
| const scoped_refptr<VideoFrame>& frame, |
| int x, |
| int y, |
| int width, |
| int height) { |
| SB_UNREFERENCED_PARAMETER(player); |
| SB_UNREFERENCED_PARAMETER(frame); |
| SB_UNREFERENCED_PARAMETER(x); |
| SB_UNREFERENCED_PARAMETER(y); |
| SB_UNREFERENCED_PARAMETER(width); |
| SB_UNREFERENCED_PARAMETER(height); |
| |
| if (frame->IsEndOfStream()) { |
| // TODO: Implement. |
| } else { |
| ID3D11Texture2D* dx_texture = |
| static_cast<ID3D11Texture2D*>(frame->native_texture()); |
| SB_UNREFERENCED_PARAMETER(dx_texture); |
| } |
| } |
| |
| } // namespace uwp |
| } // namespace shared |
| } // namespace starboard |
| |
| [Platform::MTAThread] |
| int main(Platform::Array<Platform::String^>^ args) { |
| if (!IsDebuggerPresent()) { |
| // By default, a Windows application will display a dialog box |
| // when it crashes. This is extremely undesirable when run offline. |
| // The following configures messages to be print to the console instead. |
| _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE | _CRTDBG_MODE_DEBUG); |
| _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_FILE | _CRTDBG_MODE_DEBUG); |
| _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE | _CRTDBG_MODE_DEBUG); |
| _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR); |
| } |
| |
| WSAData wsaData; |
| int init_result = WSAStartup( |
| MAKEWORD(kWinSockVersionMajor, kWinSockVersionMajor), &wsaData); |
| |
| SB_CHECK(init_result == 0); |
| // WSAStartup returns the highest version that is supported up to the version |
| // we request. |
| SB_CHECK(LOBYTE(wsaData.wVersion) == kWinSockVersionMajor && |
| HIBYTE(wsaData.wVersion) == kWinSockVersionMinor); |
| |
| HRESULT hr = MFStartup(MF_VERSION); |
| SB_DCHECK(SUCCEEDED(hr)); |
| |
| starboard::shared::win32::RegisterMainThread(); |
| |
| auto direct3DApplicationSource = ref new Direct3DApplicationSource(); |
| CoreApplication::Run(direct3DApplicationSource); |
| |
| MFShutdown(); |
| WSACleanup(); |
| |
| return main_return_value; |
| } |