#include "pch.h" #include "ReactCameraView.h" #include "ReactCameraViewManager.h" #include "NativeModules.h" #include "ReactCameraConstants.h" namespace winrt { using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Media; using namespace Windows::UI::Xaml::Controls; using namespace Windows::Devices::Enumeration; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::Media::MediaProperties; using namespace Windows::Storage; using namespace Windows::Storage::Streams; using namespace Windows::UI::Core; using namespace Windows::Media::Core; using namespace Windows::Media::Playback; using namespace Windows::Media::Capture; using namespace Windows::Graphics::Imaging; using namespace Windows::Media::Devices; using namespace Windows::System::Display; using namespace Microsoft::ReactNative; } //namespace winrt using namespace std::chrono; namespace winrt::ReactNativeCameraCPP { /*static*/ winrt::com_ptr ReactCameraView::Create() { auto view = winrt::make_self(); view->Initialize(); return view; } ReactCameraView::~ReactCameraView() { m_unloadedEventToken.revoke(); } void ReactCameraView::Initialize() { m_childElement = winrt::CaptureElement(); Children().Append(m_childElement); // RNW does not support DropView yet, so we need to manually register to Unloaded event and remove self // from the static view list m_unloadedEventToken = Unloaded(winrt::auto_revoke, [ref = get_weak()](auto const&, auto const&) { if (auto self = ref.get()) { auto unloadedAction{ self->OnUnloaded() }; unloadedAction.Completed([self](auto&& /*sender*/, AsyncStatus const /* args */) { winrt::ReactNativeCameraCPP::implementation::ReactCameraViewManager::RemoveViewFromList(self); }); } }); } void ReactCameraView::UpdateProperties(IJSValueReader const& propertyMapReader) { const JSValueObject& propertyMap = JSValue::ReadObjectFrom(propertyMapReader); for (auto const& pair : propertyMap) { auto const& propertyName = pair.first; auto const& propertyValue = pair.second; if (!propertyValue.IsNull()) { if (propertyName == "torchMode") { UpdateTorchMode(static_cast(propertyValue.Double())); } if (propertyName == "flashMode") { UpdateFlashMode(static_cast(propertyValue.Double())); } else if (propertyName == "type") { UpdateDeviceType(static_cast(propertyValue.Double())); } else if (propertyName == "keepAwake") { UpdateKeepAwake(propertyValue.Boolean()); } else if (propertyName == "aspect") { UpdateAspect(static_cast(propertyValue.Double())); } } } } IAsyncAction ReactCameraView::UpdateFilePropertiesAsync(StorageFile storageFile, std::map const& options) { auto props = co_await storageFile.Properties().GetImagePropertiesAsync(); auto searchTitle = options.find(L"title"); if (searchTitle != options.end()) { const auto& titleValue = options.at(L"title"); if (titleValue.Type() == JSValueType::String) { auto titleString = titleValue.String(); props.Title(winrt::to_hstring(titleString)); } else { throw winrt::hresult_invalid_argument(); } co_await props.SavePropertiesAsync(); } } // RNW has a bug where the numeric value is set as int in debug but double in release // ToDo: remove this function once bug https://github.com/microsoft/react-native-windows/issues/4225 is fixed. bool ReactCameraView::TryGetValueAsInt(std::map const& options, const std::wstring key, int &value) { bool found = false; auto search = options.find(key); if (search != options.end()) { const auto& searchValue = options.at(key); const bool valueIsInt = (searchValue.Type() == JSValueType::Int64); const bool valueIsDouble = (searchValue.Type() == JSValueType::Double); if (valueIsInt || valueIsDouble) { found = true; if (valueIsInt) { value = static_cast(searchValue.Int64()); } else { value = static_cast(searchValue.Double()); } } } return found; } IAsyncAction ReactCameraView::TakePictureAsync(std::map const& options , ReactPromise& result) { if (!m_isInitialized) { result.Reject(L"Media device is not initialized."); return; } auto dispatcher = Dispatcher(); co_await resume_foreground(dispatcher); // Jump to UI thread if (auto mediaCapture = m_childElement.Source()) { auto encoding = winrt::ImageEncodingProperties().CreateJpeg(); auto randomStream = winrt::InMemoryRandomAccessStream(); co_await mediaCapture.CapturePhotoToStreamAsync(encoding, randomStream); int target; if (!TryGetValueAsInt(options, L"target", target)) { result.Reject(L"target parameter not specified."); return; } if (target == ReactCameraContants::CameraCaptureTargetMemory) { // In memeory returns a base64 string for the captured image auto string = co_await GetBase64DataAsync(randomStream); JSValueObject jsObject; jsObject["data"] = winrt::to_string(string); result.Resolve(jsObject); } else { auto storageFile = co_await GetOutputStorageFileAsync(ReactCameraContants::MediaTypeImage, target); { auto photoOrientation = m_rotationHelper.GetConvertedCameraCaptureOrientation(); auto decoder = co_await winrt::BitmapDecoder::CreateAsync(randomStream); auto outputStream = co_await storageFile.OpenAsync(FileAccessMode::ReadWrite); auto encoder = co_await winrt::BitmapEncoder::CreateForTranscodingAsync(outputStream, decoder); auto bitmapTypedValue = winrt::BitmapTypedValue(winrt::box_value(photoOrientation), PropertyType::UInt16); auto properties = winrt::BitmapPropertySet(); properties.Insert(hstring(L"System.Photo.Orientation"), bitmapTypedValue); co_await encoder.BitmapProperties().SetPropertiesAsync(properties); co_await encoder.FlushAsync(); } co_await UpdateFilePropertiesAsync(storageFile, options); JSValueObject jsObject; jsObject["path"] = winrt::to_string(storageFile.Path()); result.Resolve(jsObject); } } else { result.Reject(L"Media device is not initialized."); } co_await resume_background(); } IAsyncAction ReactCameraView::RecordAsync(std::map const& options, ReactPromise& result) { if (!m_isInitialized) { result.Reject(L"Media device is not initialized."); return; } auto dispatcher = Dispatcher(); co_await resume_foreground(dispatcher); // Jump to UI thread if (auto mediaCapture = m_childElement.Source()) { int quality = static_cast(VideoEncodingQuality::Auto); TryGetValueAsInt(options, L"quality", quality); auto encodingProfile = winrt::MediaEncodingProfile(); auto encoding = encodingProfile.CreateMp4(static_cast(quality)); auto searchAudio = options.find(L"audio"); if (searchAudio != options.end()) { const auto& audioValue = options.at(L"audio"); mediaCapture.AudioDeviceController().Muted(static_cast(audioValue.Boolean())); } int totalSeconds = INT_MAX; TryGetValueAsInt(options, L"totalSeconds", totalSeconds); int target; if (!TryGetValueAsInt(options, L"target", target)) { result.Reject(L"target parameter not specified."); return; } if (target == ReactCameraContants::CameraCaptureTargetMemory) { auto randomStream = winrt::InMemoryRandomAccessStream(); m_mediaRecording = co_await mediaCapture.PrepareLowLagRecordToStreamAsync( encoding, randomStream); co_await m_mediaRecording.StartAsync(); co_await DelayStopRecording(totalSeconds); co_await WaitAndStopRecording(); auto string = co_await GetBase64DataAsync(randomStream); JSValueObject jsObject; jsObject["data"] = winrt::to_string(string); result.Resolve(jsObject); } else { auto storageFile = co_await GetOutputStorageFileAsync(ReactCameraContants::MediaTypeVideo, target); m_mediaRecording = co_await mediaCapture.PrepareLowLagRecordToStorageFileAsync( encoding, storageFile); co_await m_mediaRecording.StartAsync(); co_await DelayStopRecording(totalSeconds); co_await WaitAndStopRecording(); co_await UpdateFilePropertiesAsync(storageFile, options); JSValueObject jsObject; jsObject["path"] = winrt::to_string(storageFile.Path()); result.Resolve(jsObject); } } else { result.Reject("No media capture device found"); } co_await resume_background(); } // async function to wait for specified seconds before signaling to stop recording IAsyncAction ReactCameraView::DelayStopRecording(int totalRecordingInSecs) { ResetEvent(m_signal.get()); std::chrono::duration secs(totalRecordingInSecs); co_await secs; SetEvent(m_signal.get()); } IAsyncAction ReactCameraView::WaitAndStopRecording() { co_await resume_on_signal(m_signal.get()); auto dispatcher = Dispatcher(); co_await resume_foreground(dispatcher); co_await m_mediaRecording.StopAsync(); co_await resume_background(); } // Switch between front and back cameras, need to clean up and reinitialize the mediaCapture object fire_and_forget ReactCameraView::UpdateDeviceType(int type) { winrt::Windows::Devices::Enumeration::Panel newPanelType = static_cast(type); if (m_panelType == newPanelType && m_isInitialized) { return; } m_panelType = newPanelType; if (m_isInitialized) { co_await CleanupMediaCaptureAsync(); } co_await InitializeAsync(); } // Request monitor to not turn off if keepAwake is true void ReactCameraView::UpdateKeepAwake(bool keepAwake) { if (m_keepAwake != keepAwake) { m_keepAwake = keepAwake; if (m_keepAwake) { if (m_displayRequest == nullptr) { m_displayRequest = DisplayRequest(); m_displayRequest.RequestActive(); } } else { m_displayRequest.RequestRelease(); } } } void ReactCameraView::UpdateTorchMode(int torchMode) { m_torchMode = torchMode; if (auto mediaCapture = m_childElement.Source()) { auto torchControl = mediaCapture.VideoDeviceController().TorchControl(); if (torchControl.Supported()) { torchControl.Enabled(torchMode == ReactCameraContants::CameraTorchModeOn); } } } void ReactCameraView::UpdateFlashMode(int flashMode) { m_flashMode = flashMode; if (auto mediaCapture = m_childElement.Source()) { auto flashControl = mediaCapture.VideoDeviceController().FlashControl(); if (flashControl.Supported()) { flashControl.Enabled(flashMode == ReactCameraContants::CameraFlashModeOn); flashControl.Auto(flashMode == ReactCameraContants::CameraFlashModeAuto); } } } void ReactCameraView::UpdateAspect(int aspect) { switch (aspect) { case ReactCameraContants::CameraAspectFill: m_childElement.Stretch(Stretch::Uniform); break; case ReactCameraContants::CameraAspectFit: m_childElement.Stretch(Stretch::UniformToFill); break; case ReactCameraContants::CameraAspectStretch: m_childElement.Stretch(Stretch::Fill); break; default: m_childElement.Stretch(Stretch::None); break; } } // Intialization takes care few things below: // 1. Register rotation helper to update preview if rotation changes. // 2. Takes care connected standby scenarios to cleanup and reintialize when suspend/resume IAsyncAction ReactCameraView::InitializeAsync() { try { auto device = co_await FindCameraDeviceByPanelAsync(); if (device != nullptr) { auto settings = winrt::Windows::Media::Capture::MediaCaptureInitializationSettings(); settings.VideoDeviceId(device.Id()); auto mediaCapture = winrt::Windows::Media::Capture::MediaCapture(); co_await mediaCapture.InitializeAsync(settings); m_childElement.Source(mediaCapture); UpdateTorchMode(m_torchMode); UpdateFlashMode(m_flashMode); UpdateKeepAwake(m_keepAwake); co_await mediaCapture.StartPreviewAsync(); m_rotationHelper = CameraRotationHelper(device.EnclosureLocation()); m_rotationEventToken = m_rotationHelper.OrientationChanged([ref = get_weak()](const auto&, const bool updatePreview) { if (auto self = ref.get()) { self->OnOrientationChanged(updatePreview); } }); co_await UpdatePreviewOrientationAsync(); m_applicationSuspendingEventToken = winrt::Application::Current().Suspending(winrt::auto_revoke, [ref = get_weak()](auto const&, auto const&) { if (auto self = ref.get()) { self->OnApplicationSuspending(); } }); m_applicationResumingEventToken = winrt::Application::Current().Resuming(winrt::auto_revoke, [ref = get_weak()](auto const&, auto const&) { if (auto self = ref.get()) { self->OnApplicationResuming(); } }); m_isInitialized = true; } } catch (winrt::hresult_error const&) { m_isInitialized = false; } } IAsyncAction ReactCameraView::CleanupMediaCaptureAsync() { if (m_isInitialized) { SetEvent(m_signal.get()); // In case recording is still going on if (auto mediaCapture = m_childElement.Source()) { co_await mediaCapture.StopPreviewAsync(); if (m_rotationHelper != nullptr) { m_rotationHelper.OrientationChanged(m_rotationEventToken); m_rotationHelper = nullptr; } m_childElement.Source(nullptr); } m_isInitialized = false; } } IAsyncOperation ReactCameraView::FindCameraDeviceByPanelAsync() { // Get available devices for capturing pictures auto allVideoDevices = co_await winrt::DeviceInformation::FindAllAsync(winrt::DeviceClass::VideoCapture); for (auto cameraDeviceInfo : allVideoDevices) { if (cameraDeviceInfo.EnclosureLocation() != nullptr && cameraDeviceInfo.EnclosureLocation().Panel() == m_panelType) { co_return cameraDeviceInfo; } } // Nothing matched, just return the first if (allVideoDevices.Size() > 0) { co_return allVideoDevices.GetAt(0); } // We didn't find any devices, so return a null instance co_return nullptr; } // update preview if display orientation changes. void ReactCameraView::OnOrientationChanged(const bool updatePreview) { if (updatePreview) { UpdatePreviewOrientationAsync(); } } void ReactCameraView::OnApplicationSuspending() { if (m_keepAwake) { m_displayRequest.RequestRelease(); } CleanupMediaCaptureAsync(); } IAsyncAction ReactCameraView::OnUnloaded() { co_await CleanupMediaCaptureAsync(); } void ReactCameraView::OnApplicationResuming() { if (m_keepAwake) { m_displayRequest.RequestActive(); } InitializeAsync(); } // update preview considering current orientation IAsyncAction ReactCameraView::UpdatePreviewOrientationAsync() { if (m_isInitialized) { if (auto mediaCapture = m_childElement.Source()) { const GUID RotationKey = { 0xC380465D, 0x2271, 0x428C, {0x9B, 0x83, 0xEC, 0xEA, 0x3B, 0x4A, 0x85, 0xC1} }; auto props = mediaCapture.VideoDeviceController().GetMediaStreamProperties(MediaStreamType::VideoPreview); props.Properties().Insert(RotationKey, winrt::box_value(m_rotationHelper.GetCameraPreviewClockwiseDegrees())); co_await mediaCapture.SetEncodingPropertiesAsync(MediaStreamType::VideoPreview, props, nullptr); } } } IAsyncOperation ReactCameraView::GetBase64DataAsync(winrt::Windows::Storage::Streams::IRandomAccessStream stream) { auto streamSize = static_cast(stream.Size()); auto inputStream = stream.GetInputStreamAt(0); auto dataReader = winrt::Windows::Storage::Streams::DataReader(inputStream); co_await dataReader.LoadAsync(streamSize); auto buffer = dataReader.ReadBuffer(streamSize); co_return winrt::Windows::Security::Cryptography::CryptographicBuffer::EncodeToBase64String(buffer); } IAsyncOperation ReactCameraView::GetOutputStorageFileAsync(int type, int target) { auto ext = type == ReactCameraContants::MediaTypeImage ? ".jpg" : ".mp4"; auto now = winrt::clock::now(); auto ttnow = winrt::clock::to_time_t(now); struct tm time; _localtime64_s(&time, &ttnow); wchar_t buf[35]; swprintf_s(buf, ARRAYSIZE(buf), L"%04d%02d%02d_%02d%02d%02d", 1900 + time.tm_year, 1 + time.tm_mon, time.tm_mday, time.tm_hour, time.tm_min, time.tm_sec); auto filename = winrt::to_hstring(buf) + winrt::to_hstring(ext); switch (target) { case ReactCameraContants::CameraCaptureTargetMemory: case ReactCameraContants::CameraCaptureTargetTemp: return winrt::ApplicationData::Current().TemporaryFolder().CreateFileAsync(filename); case ReactCameraContants::CameraCaptureTargetCameraRoll: return winrt::KnownFolders::CameraRoll().CreateFileAsync(filename); case ReactCameraContants::CameraCaptureTargetDisk: if (type == ReactCameraContants::MediaTypeImage) { return winrt::KnownFolders::PicturesLibrary().CreateFileAsync(filename); } else { return winrt::KnownFolders::VideosLibrary().CreateFileAsync(filename); } } return nullptr; } void ReactCameraView::SetContext(winrt::Microsoft::ReactNative::IReactContext const& reactContext) { m_reactContext = reactContext; } } // namespace winrt::ReactNativeVideoCPP