542 lines
21 KiB
C++
542 lines
21 KiB
C++
#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> ReactCameraView::Create() {
|
|
auto view = winrt::make_self<ReactCameraView>();
|
|
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<int>(propertyValue.Double()));
|
|
}
|
|
if (propertyName == "flashMode") {
|
|
UpdateFlashMode(static_cast<int>(propertyValue.Double()));
|
|
}
|
|
else if (propertyName == "type") {
|
|
UpdateDeviceType(static_cast<int>(propertyValue.Double()));
|
|
}
|
|
else if (propertyName == "keepAwake") {
|
|
UpdateKeepAwake(propertyValue.Boolean());
|
|
}
|
|
else if (propertyName == "aspect") {
|
|
UpdateAspect(static_cast<int>(propertyValue.Double()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
IAsyncAction ReactCameraView::UpdateFilePropertiesAsync(StorageFile storageFile, std::map<std::wstring, JSValue> 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<std::wstring, JSValue> 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<int>(searchValue.Int64());
|
|
}
|
|
else
|
|
{
|
|
value = static_cast<int>(searchValue.Double());
|
|
}
|
|
}
|
|
}
|
|
|
|
return found;
|
|
}
|
|
|
|
IAsyncAction ReactCameraView::TakePictureAsync(std::map<std::wstring, JSValue> const& options
|
|
, ReactPromise<JSValueObject>& 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<std::wstring, JSValue> const& options, ReactPromise<JSValueObject>& 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<int>(VideoEncodingQuality::Auto);
|
|
TryGetValueAsInt(options, L"quality", quality);
|
|
auto encodingProfile = winrt::MediaEncodingProfile();
|
|
auto encoding = encodingProfile.CreateMp4(static_cast<VideoEncodingQuality>(quality));
|
|
|
|
auto searchAudio = options.find(L"audio");
|
|
if (searchAudio != options.end())
|
|
{
|
|
const auto& audioValue = options.at(L"audio");
|
|
mediaCapture.AudioDeviceController().Muted(static_cast<bool>(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<int> 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<winrt::Windows::Devices::Enumeration::Panel>(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<winrt::DeviceInformation> 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<winrt::hstring> ReactCameraView::GetBase64DataAsync(winrt::Windows::Storage::Streams::IRandomAccessStream stream)
|
|
{
|
|
auto streamSize = static_cast<uint32_t>(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<winrt::StorageFile> 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
|