react-native-camera_edit/windows/ReactNativeCameraCPP/ReactCameraView.cpp
2025-07-09 11:33:00 +09:00

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