From 1c627490c71103f24f6ad3ede0cf1446dfde635f Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Mon, 3 Dec 2018 10:50:37 -0500 Subject: [PATCH] Adds react-native-windows UWP support (#862) * Adds react-native-windows UWP support This adds basic support for capturing photos and videos from either the front or back panel cameras, with some support for video quality, orientation, etc. This also uses ZXing.Net to support barcode scanning from the video preview frames. It also supports torch and flash modes. Videos and photos saved to disk (or camera roll / temporary folder) also supports some file metadata (e.g., lat/long). There are a number of features that have not yet been implemented: - Support `playSoundOnCapture` for default shutter sounds - Add orientation metadata properties on photo / video files - Support all barcode formats in ZXing.Net - Additional file metadata (like description) - Photo quality settings with `quality` and `jpegQuality` - Image post-processing with `mirrorImage` and `fixOrientation` - Support event listeners for `onZoomChanged` & `onFocusChanged` - Device authorization checks as supported on iOS * Updating NuGet packages for RNCamera UWP project * Minor fixes for legacy RCTCamera implementation * hack(CameraForView): default camera to any when panel info not available --- src/Camera.js | 9 + windows/.gitignore | 78 ++++ windows/.npmignore | 9 + windows/RNCamera.sln | 115 +++++ windows/RNCamera/BarcodeFormatExtensions.cs | 39 ++ windows/RNCamera/CameraForView.cs | 396 ++++++++++++++++ windows/RNCamera/CameraForViewManager.cs | 52 +++ windows/RNCamera/CameraRotationHelper.cs | 229 ++++++++++ windows/RNCamera/GeolocationHelper.cs | 72 +++ windows/RNCamera/JObjectExtensions.cs | 13 + windows/RNCamera/MediaCaptureExtensions.cs | 26 ++ windows/RNCamera/Properties/AssemblyInfo.cs | 30 ++ windows/RNCamera/Properties/RNCamera.rd.xml | 28 ++ windows/RNCamera/RCTCameraModule.cs | 473 ++++++++++++++++++++ windows/RNCamera/RCTCameraViewManager.cs | 188 ++++++++ windows/RNCamera/RNCamera.csproj | 174 +++++++ windows/RNCamera/RNCameraPackage.cs | 56 +++ 17 files changed, 1987 insertions(+) create mode 100644 windows/.gitignore create mode 100644 windows/.npmignore create mode 100644 windows/RNCamera.sln create mode 100644 windows/RNCamera/BarcodeFormatExtensions.cs create mode 100644 windows/RNCamera/CameraForView.cs create mode 100644 windows/RNCamera/CameraForViewManager.cs create mode 100644 windows/RNCamera/CameraRotationHelper.cs create mode 100644 windows/RNCamera/GeolocationHelper.cs create mode 100644 windows/RNCamera/JObjectExtensions.cs create mode 100644 windows/RNCamera/MediaCaptureExtensions.cs create mode 100644 windows/RNCamera/Properties/AssemblyInfo.cs create mode 100644 windows/RNCamera/Properties/RNCamera.rd.xml create mode 100644 windows/RNCamera/RCTCameraModule.cs create mode 100644 windows/RNCamera/RCTCameraViewManager.cs create mode 100644 windows/RNCamera/RNCamera.csproj create mode 100644 windows/RNCamera/RNCameraPackage.cs diff --git a/src/Camera.js b/src/Camera.js index 293f56014..bd6827de9 100644 --- a/src/Camera.js +++ b/src/Camera.js @@ -230,6 +230,7 @@ export default class Camera extends Component { this.cameraBarCodeReadListener = Platform.select({ ios: NativeAppEventEmitter.addListener('CameraBarCodeRead', this._onBarCodeRead), android: DeviceEventEmitter.addListener('CameraBarCodeReadAndroid', this._onBarCodeRead), + windows: DeviceEventEmitter.addListener('CameraBarCodeReadWindows', this._onBarCodeRead), }); } } @@ -315,6 +316,10 @@ export default class Camera extends Component { ...options, }; + if (Platform.OS === 'windows') { + options['view'] = this._cameraHandle; + } + if (options.mode === Camera.constants.CaptureMode.video) { options.totalSeconds = options.totalSeconds > -1 ? options.totalSeconds : -1; options.preferredTimeScale = options.preferredTimeScale || 30; @@ -367,6 +372,10 @@ export default class Camera extends Component { return CameraManager.hasFlash({ type: props.type, }); + } else if (Platform.OS === 'windows') { + return CameraManager.hasFlash({ + view: this._cameraHandle, + }); } return CameraManager.hasFlash(); } diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 000000000..cbf7e7f42 --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,78 @@ +*AppPackages* +*BundleArtifacts* +*ReactAssets* + +#OS junk files +[Tt]humbs.db +*.DS_Store + +#Visual Studio files +*.[Oo]bj +*.user +*.aps +*.pch +*.vspscc +*.vssscc +*_i.c +*_p.c +*.ncb +*.suo +*.tlb +*.tlh +*.bak +*.[Cc]ache +*.ilk +*.log +*.lib +*.sbr +*.sdf +*.opensdf +*.opendb +*.unsuccessfulbuild +ipch/ +[Oo]bj/ +[Bb]in +[Dd]ebug*/ +[Rr]elease*/ +Ankh.NoLoad + +#MonoDevelop +*.pidb +*.userprefs + +#Tooling +_ReSharper*/ +*.resharper +[Tt]est[Rr]esult* +*.sass-cache + +#Project files +[Bb]uild/ + +#Subversion files +.svn + +# Office Temp Files +~$* + +# vim Temp Files +*~ + +#NuGet +packages/ +*.nupkg + +#ncrunch +*ncrunch* +*crunch*.local.xml + +# visual studio database projects +*.dbmdl + +#Test files +*.testsettings + +#Other files +*.DotSettings +.vs/ +*project.lock.json diff --git a/windows/.npmignore b/windows/.npmignore new file mode 100644 index 000000000..dd626ca0a --- /dev/null +++ b/windows/.npmignore @@ -0,0 +1,9 @@ + +# Make sure we don't publish build artifacts to NPM +ARM/ +Debug/ +x64/ +x86/ +bin/ +obj/ +.vs/ diff --git a/windows/RNCamera.sln b/windows/RNCamera.sln new file mode 100644 index 000000000..2c5c993ed --- /dev/null +++ b/windows/RNCamera.sln @@ -0,0 +1,115 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26730.3 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RNCamera", "RNCamera\RNCamera.csproj", "{F11038F0-88E5-11E7-BA7C-E36C7490591A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactNative", "..\node_modules\react-native-windows\ReactWindows\ReactNative\ReactNative.csproj", "{C7673AD5-E3AA-468C-A5FD-FA38154E205C}" +EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "ReactNative.Shared", "..\node_modules\react-native-windows\ReactWindows\ReactNative.Shared\ReactNative.Shared.shproj", "{EEA8B852-4D07-48E1-8294-A21AB5909FE5}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ChakraBridge", "..\node_modules\react-native-windows\ReactWindows\ChakraBridge\ChakraBridge.vcxproj", "{4B72C796-16D5-4E3A-81C0-3E36F531E578}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactNativeWebViewBridge", "..\node_modules\react-native-windows\ReactWindows\ReactNativeWebViewBridge\ReactNativeWebViewBridge.csproj", "{7596216B-669C-41F8-86DA-F3637F6545C0}" +EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Facebook.Yoga.Shared", "..\node_modules\react-native-windows\Yoga\csharp\Facebook.Yoga\Facebook.Yoga.Shared.shproj", "{91C42D32-291D-4B72-90B4-551663D60B8B}" +EndProject +Global + GlobalSection(SharedMSBuildProjectFiles) = preSolution + ..\node_modules\react-native-windows\Yoga\csharp\Facebook.Yoga\Facebook.Yoga.Shared.projitems*{91c42d32-291d-4b72-90b4-551663d60b8b}*SharedItemsImports = 13 + ..\node_modules\react-native-windows\ReactWindows\ReactNative.Shared\ReactNative.Shared.projitems*{c7673ad5-e3aa-468c-a5fd-fa38154e205c}*SharedItemsImports = 4 + ..\node_modules\react-native-windows\Yoga\csharp\Facebook.Yoga\Facebook.Yoga.Shared.projitems*{c7673ad5-e3aa-468c-a5fd-fa38154e205c}*SharedItemsImports = 4 + ..\node_modules\react-native-windows\ReactWindows\ReactNative.Shared\ReactNative.Shared.projitems*{eea8b852-4d07-48e1-8294-a21ab5909fe5}*SharedItemsImports = 13 + EndGlobalSection + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM = Debug|ARM + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Development|ARM = Development|ARM + Development|x64 = Development|x64 + Development|x86 = Development|x86 + Release|ARM = Release|ARM + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F11038F0-88E5-11E7-BA7C-E36C7490591A}.Debug|ARM.ActiveCfg = Debug|ARM + {F11038F0-88E5-11E7-BA7C-E36C7490591A}.Debug|ARM.Build.0 = Debug|ARM + {F11038F0-88E5-11E7-BA7C-E36C7490591A}.Debug|x64.ActiveCfg = Debug|x64 + {F11038F0-88E5-11E7-BA7C-E36C7490591A}.Debug|x64.Build.0 = Debug|x64 + {F11038F0-88E5-11E7-BA7C-E36C7490591A}.Debug|x86.ActiveCfg = Debug|x86 + {F11038F0-88E5-11E7-BA7C-E36C7490591A}.Debug|x86.Build.0 = Debug|x86 + {F11038F0-88E5-11E7-BA7C-E36C7490591A}.Development|ARM.ActiveCfg = Development|ARM + {F11038F0-88E5-11E7-BA7C-E36C7490591A}.Development|ARM.Build.0 = Development|ARM + {F11038F0-88E5-11E7-BA7C-E36C7490591A}.Development|x64.ActiveCfg = Development|x64 + {F11038F0-88E5-11E7-BA7C-E36C7490591A}.Development|x64.Build.0 = Development|x64 + {F11038F0-88E5-11E7-BA7C-E36C7490591A}.Development|x86.ActiveCfg = Development|x86 + {F11038F0-88E5-11E7-BA7C-E36C7490591A}.Development|x86.Build.0 = Development|x86 + {F11038F0-88E5-11E7-BA7C-E36C7490591A}.Release|ARM.ActiveCfg = Release|ARM + {F11038F0-88E5-11E7-BA7C-E36C7490591A}.Release|ARM.Build.0 = Release|ARM + {F11038F0-88E5-11E7-BA7C-E36C7490591A}.Release|x64.ActiveCfg = Release|x64 + {F11038F0-88E5-11E7-BA7C-E36C7490591A}.Release|x64.Build.0 = Release|x64 + {F11038F0-88E5-11E7-BA7C-E36C7490591A}.Release|x86.ActiveCfg = Release|x86 + {F11038F0-88E5-11E7-BA7C-E36C7490591A}.Release|x86.Build.0 = Release|x86 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug|ARM.ActiveCfg = Debug|ARM + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug|ARM.Build.0 = Debug|ARM + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug|x64.ActiveCfg = Debug|x64 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug|x64.Build.0 = Debug|x64 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug|x86.ActiveCfg = Debug|x86 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug|x86.Build.0 = Debug|x86 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Development|ARM.ActiveCfg = Debug|ARM + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Development|ARM.Build.0 = Debug|ARM + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Development|x64.ActiveCfg = Debug|x64 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Development|x64.Build.0 = Debug|x64 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Development|x86.ActiveCfg = Debug|x86 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Development|x86.Build.0 = Debug|x86 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Release|ARM.ActiveCfg = Release|ARM + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Release|ARM.Build.0 = Release|ARM + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Release|x64.ActiveCfg = Release|x64 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Release|x64.Build.0 = Release|x64 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Release|x86.ActiveCfg = Release|x86 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Release|x86.Build.0 = Release|x86 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Debug|ARM.ActiveCfg = Debug|ARM + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Debug|ARM.Build.0 = Debug|ARM + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Debug|x64.ActiveCfg = Debug|x64 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Debug|x64.Build.0 = Debug|x64 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Debug|x86.ActiveCfg = Debug|Win32 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Debug|x86.Build.0 = Debug|Win32 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Development|ARM.ActiveCfg = Debug|ARM + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Development|ARM.Build.0 = Debug|ARM + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Development|x64.ActiveCfg = Debug|x64 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Development|x64.Build.0 = Debug|x64 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Development|x86.ActiveCfg = Debug|Win32 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Development|x86.Build.0 = Debug|Win32 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Release|ARM.ActiveCfg = Release|ARM + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Release|ARM.Build.0 = Release|ARM + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Release|x64.ActiveCfg = Release|x64 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Release|x64.Build.0 = Release|x64 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Release|x86.ActiveCfg = Release|Win32 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Release|x86.Build.0 = Release|Win32 + {7596216B-669C-41F8-86DA-F3637F6545C0}.Debug|ARM.ActiveCfg = Debug|ARM + {7596216B-669C-41F8-86DA-F3637F6545C0}.Debug|ARM.Build.0 = Debug|ARM + {7596216B-669C-41F8-86DA-F3637F6545C0}.Debug|x64.ActiveCfg = Debug|x64 + {7596216B-669C-41F8-86DA-F3637F6545C0}.Debug|x64.Build.0 = Debug|x64 + {7596216B-669C-41F8-86DA-F3637F6545C0}.Debug|x86.ActiveCfg = Debug|x86 + {7596216B-669C-41F8-86DA-F3637F6545C0}.Debug|x86.Build.0 = Debug|x86 + {7596216B-669C-41F8-86DA-F3637F6545C0}.Development|ARM.ActiveCfg = Debug|ARM + {7596216B-669C-41F8-86DA-F3637F6545C0}.Development|ARM.Build.0 = Debug|ARM + {7596216B-669C-41F8-86DA-F3637F6545C0}.Development|x64.ActiveCfg = Debug|x64 + {7596216B-669C-41F8-86DA-F3637F6545C0}.Development|x64.Build.0 = Debug|x64 + {7596216B-669C-41F8-86DA-F3637F6545C0}.Development|x86.ActiveCfg = Debug|x86 + {7596216B-669C-41F8-86DA-F3637F6545C0}.Development|x86.Build.0 = Debug|x86 + {7596216B-669C-41F8-86DA-F3637F6545C0}.Release|ARM.ActiveCfg = Release|ARM + {7596216B-669C-41F8-86DA-F3637F6545C0}.Release|ARM.Build.0 = Release|ARM + {7596216B-669C-41F8-86DA-F3637F6545C0}.Release|x64.ActiveCfg = Release|x64 + {7596216B-669C-41F8-86DA-F3637F6545C0}.Release|x64.Build.0 = Release|x64 + {7596216B-669C-41F8-86DA-F3637F6545C0}.Release|x86.ActiveCfg = Release|x86 + {7596216B-669C-41F8-86DA-F3637F6545C0}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B259F75D-E772-4C88-973E-F0EF45B3F422} + EndGlobalSection +EndGlobal diff --git a/windows/RNCamera/BarcodeFormatExtensions.cs b/windows/RNCamera/BarcodeFormatExtensions.cs new file mode 100644 index 000000000..b07a24c41 --- /dev/null +++ b/windows/RNCamera/BarcodeFormatExtensions.cs @@ -0,0 +1,39 @@ +using System; +using ZXing; + +namespace RNCamera +{ + static class BarcodeFormatExtensions + { + public static string GetName(this BarcodeFormat barcodeFormat) + { + switch (barcodeFormat) + { + case BarcodeFormat.AZTEC: + return "aztec"; + case BarcodeFormat.CODE_39: + return "code39"; + case BarcodeFormat.CODE_93: + return "code93"; + case BarcodeFormat.CODE_128: + return "code128"; + case BarcodeFormat.DATA_MATRIX: + return "datamatrix"; + case BarcodeFormat.EAN_8: + return "ean8"; + case BarcodeFormat.EAN_13: + return "ean13"; + case BarcodeFormat.ITF: + return "interleaved2of5"; + case BarcodeFormat.PDF_417: + return "pdf417"; + case BarcodeFormat.QR_CODE: + return "qr"; + case BarcodeFormat.UPC_E: + return "upce"; + default: + throw new NotImplementedException(); + } + } + } +} diff --git a/windows/RNCamera/CameraForView.cs b/windows/RNCamera/CameraForView.cs new file mode 100644 index 000000000..da88b053d --- /dev/null +++ b/windows/RNCamera/CameraForView.cs @@ -0,0 +1,396 @@ +using ReactNative.Bridge; +using System; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using Windows.ApplicationModel.Core; +using Windows.Devices.Enumeration; +using Windows.Devices.Sensors; +using Windows.Graphics.Imaging; +using Windows.Media; +using Windows.Media.Capture; +using Windows.Media.MediaProperties; +using Windows.Storage.FileProperties; +using Windows.System.Display; +using Windows.UI.Core; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using ZXing; + +namespace RNCamera +{ + class CameraForView : IAsyncDisposable, ILifecycleEventListener + { + private static readonly Guid RotationKey = new Guid("C380465D-2271-428C-9B83-ECEA3B4A85C1"); + + private readonly object _initializationGate = new object(); + + private Windows.Devices.Enumeration.Panel? _panel; + private CameraRotationHelper _rotationHelper; + + private DisplayRequest _displayRequest; + private bool _keepAwake; + + private bool _barcodeScanningEnabled; + private SerialDisposable _barcodeScanningSubscription = new SerialDisposable(); + + private int _flashMode; + private int _torchMode; + + private bool _wasInitializedOnSuspend; + private Task _initializationTask; + + public CameraForView(CaptureElement captureElement) + { + CaptureElement = captureElement; + } + + public event Action BarcodeScanned; + + public CaptureElement CaptureElement { get; } + + public MediaCapture MediaCapture { get; set; } + + public bool IsInitialized { get; private set; } + + public BarcodeReader BarcodeReader { get; set; } + + public bool BarcodeScanningEnabled + { + get + { + return _barcodeScanningEnabled; + } + set + { + if (_barcodeScanningEnabled != value) + { + var wasBarcodeScanningEnabled = _barcodeScanningEnabled; + _barcodeScanningEnabled = value; + if (_barcodeScanningEnabled && BarcodeReader == null) + { + BarcodeReader = new BarcodeReader(); + } + + if (_barcodeScanningEnabled && IsInitialized) + { + _barcodeScanningSubscription.Disposable = + GetBarcodeScanningObservable().Subscribe(result => + { + BarcodeScanned?.Invoke(result); + }); + } + else if (wasBarcodeScanningEnabled && IsInitialized) + { + _barcodeScanningSubscription.Disposable = Disposable.Empty; + } + } + } + } + + public bool KeepAwake + { + get + { + return _keepAwake; + } + set + { + if (_keepAwake != value) + { + _keepAwake = value; + if (_keepAwake) + { + if (_displayRequest == null) + { + _displayRequest = new DisplayRequest(); + } + + _displayRequest.RequestActive(); + } + else if (_displayRequest != null) + { + _displayRequest.RequestRelease(); + } + } + } + } + + public int FlashMode + { + get + { + return _flashMode; + } + set + { + _flashMode = value; + if (IsInitialized) + { + MediaCapture.SetFlashMode(_flashMode); + } + } + } + + public int TorchMode + { + get + { + return _torchMode; + } + set + { + _torchMode = value; + if (IsInitialized) + { + MediaCapture.SetTorchMode(_torchMode); + } + } + } + + public int Orientation { get; set; } + + public async Task GetCameraCaptureOrientationAsync() + { + var result = new TaskCompletionSource(); + + await CoreApplication.MainView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + var orientation = _rotationHelper.GetCameraCaptureOrientation(); + Task.Run(() => result.SetResult(orientation)); + }).AsTask().ConfigureAwait(false); + + return await result.Task.ConfigureAwait(false); + } + + public Task InitializeAsync() + { + var initializationTask = default(Task); + if (!IsInitialized && _initializationTask == null) + { + lock (_initializationGate) + { + // If not initialized and no running initialization, start one + if (!IsInitialized && _initializationTask == null) + { + MediaCapture = new MediaCapture(); + _initializationTask = InitializeMediaCaptureAsync(); + } + + // Set the current task or null if initialized + initializationTask = _initializationTask; + } + } + + return initializationTask ?? Task.CompletedTask; + } + + public async Task UpdatePanelAsync(Windows.Devices.Enumeration.Panel panel) + { + if (panel == _panel) + { + return; + } + + _panel = panel; + if (IsInitialized) + { + await CleanupMediaCaptureAsync(); + await InitializeAsync().ConfigureAwait(false); + } + } + + public void OnSuspend() + { + if (KeepAwake) + { + _displayRequest.RequestRelease(); + } + + // Blocking to ensure cleanup before suspend + _wasInitializedOnSuspend = IsInitialized; + if (_wasInitializedOnSuspend) + { + CleanupMediaCaptureAsync().Wait(); + } + } + + public async void OnResume() + { + if (KeepAwake) + { + _displayRequest.RequestActive(); + } + + if (_wasInitializedOnSuspend) + { + await InitializeAsync().ConfigureAwait(false); + } + } + + public void OnDestroy() + { + // Blocking to ensure cleanup before dispose + DisposeAsync().Wait(); + } + + public async Task DisposeAsync() + { + if (IsInitialized) + { + await CleanupMediaCaptureAsync().ConfigureAwait(false); + } + + if (KeepAwake) + { + _displayRequest.RequestRelease(); + } + + _barcodeScanningSubscription.Dispose(); + } + + private async Task CleanupMediaCaptureAsync() + { + // Wait for initialization to complete + var initializationTask = default(Task); + lock (_initializationGate) + { + initializationTask = _initializationTask; + } + + // This blocking call should rarely occur + if (initializationTask != null) + { + await initializationTask; + } + + // Cancel current barcode scanning subscription + _barcodeScanningSubscription.Disposable = Disposable.Empty; + + // Stop preview + // TODO: uncomment when async dispose is supported + // await MediaCapture.StopPreviewAsync().AsTask().ConfigureAwait(false); + + // Dispose media capture + MediaCapture.Dispose(); + + // Remove orientation subscription + if (IsInitialized) + { + _rotationHelper.OrientationChanged -= OnOrientationChanged; + } + + IsInitialized = false; + + // TODO: race condition on re-initializing MediaCapture? + // E.g., set new panel while starting record + MediaCapture = null; + } + + private async Task InitializeMediaCaptureAsync() + { + // Do not use ConfigureAwait(false), subsequent calls must come from Dispatcher thread + var devices = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture); + var device = _panel.HasValue + ? devices.FirstOrDefault(d => d.EnclosureLocation?.Panel == _panel) + : devices.FirstOrDefault(); + + // TODO: remove this hack, it defaults the camera to any camera if it cannot find one for a specific panel + device = device ?? devices.FirstOrDefault(); + + if (device == null) + { + throw new InvalidOperationException("Could not find camera device."); + } + + _rotationHelper = new CameraRotationHelper(device.EnclosureLocation); + _rotationHelper.OrientationChanged += OnOrientationChanged; + + // Initialize for panel + var settings = new MediaCaptureInitializationSettings + { + VideoDeviceId = device.Id, + }; + + // Do not use ConfigureAwait(false), subsequent calls must come from Dispatcher thread + await MediaCapture.InitializeAsync(settings); + + // Set flash modes + MediaCapture.SetFlashMode(FlashMode); + MediaCapture.SetTorchMode(TorchMode); + + // Set to capture element + CaptureElement.Source = MediaCapture; + // Mirror for front facing camera + CaptureElement.FlowDirection = _panel == Windows.Devices.Enumeration.Panel.Front + ? FlowDirection.RightToLeft + : FlowDirection.LeftToRight; + + // Start preview + // Do not `ConfigureAwait(false), orientation must be set on Dispatcher thread + await MediaCapture.StartPreviewAsync(); + + // Set preview rotation + await UpdatePreviewOrientationAsync().ConfigureAwait(false); + + // Start barcode scanning + if (BarcodeScanningEnabled) + { + _barcodeScanningSubscription.Disposable = + GetBarcodeScanningObservable().Subscribe(result => + { + BarcodeScanned?.Invoke(result); + }); + } + + IsInitialized = true; + + lock (_initializationGate) + { + _initializationTask = null; + } + } + + private IObservable GetBarcodeScanningObservable() + { + return Observable.Create( + new Func, CancellationToken, Task>(DoBarcodeScanningAsync)); + } + + private async Task DoBarcodeScanningAsync(IObserver observer, CancellationToken token) + { + var previewProperties = (VideoEncodingProperties)MediaCapture.VideoDeviceController.GetMediaStreamProperties(MediaStreamType.VideoPreview); + while (!token.IsCancellationRequested) + { + var videoFrame = new VideoFrame(BitmapPixelFormat.Bgra8, (int)previewProperties.Width, (int)previewProperties.Height); + var previewFrame = await MediaCapture.GetPreviewFrameAsync(videoFrame).AsTask().ConfigureAwait(false); + if (previewFrame.SoftwareBitmap != null) + { + var result = BarcodeReader.Decode(previewFrame.SoftwareBitmap); + if (result != null) + { + observer.OnNext(result); + } + } + } + } + + private async void OnOrientationChanged(object sender, bool updatePreview) + { + if (updatePreview) + { + await UpdatePreviewOrientationAsync().ConfigureAwait(false); + } + } + + private async Task UpdatePreviewOrientationAsync() + { + var rotation = _rotationHelper.GetCameraPreviewOrientation(); + var props = MediaCapture.VideoDeviceController.GetMediaStreamProperties(MediaStreamType.VideoPreview); + props.Properties.Add(RotationKey, CameraRotationHelper.ConvertSimpleOrientationToClockwiseDegrees(rotation)); + await MediaCapture.SetEncodingPropertiesAsync(MediaStreamType.VideoPreview, props, null).AsTask().ConfigureAwait(false); + } + } +} diff --git a/windows/RNCamera/CameraForViewManager.cs b/windows/RNCamera/CameraForViewManager.cs new file mode 100644 index 000000000..e29481256 --- /dev/null +++ b/windows/RNCamera/CameraForViewManager.cs @@ -0,0 +1,52 @@ +using ReactNative.UIManager; +using System.Collections.Generic; +using Windows.UI.Xaml.Controls; + +namespace RNCamera +{ + class CameraForViewManager + { + private readonly object _gate = new object(); + + private readonly IDictionary _cameras = + new Dictionary(); + + public CameraForView GetCameraForView(int viewTag) + { + CameraForView result; + if (!_cameras.TryGetValue(viewTag, out result)) + { + return null; + } + + return result; + } + + public CameraForView GetOrCreateCameraForView(CaptureElement view) + { + var viewTag = view.GetTag(); + var reactContext = view.GetReactContext(); + CameraForView result; + if (!_cameras.TryGetValue(viewTag, out result)) + { + result = new CameraForView(view); + _cameras.Add(viewTag, result); + reactContext.AddLifecycleEventListener(result); + } + + return result; + } + + public void DropCameraForView(CaptureElement view) + { + var viewTag = view.GetTag(); + var camera = GetCameraForView(viewTag); + if (camera != null) + { + _cameras.Remove(viewTag); + var reactContext = view.GetReactContext(); + reactContext.RemoveLifecycleEventListener(camera); + } + } + } +} diff --git a/windows/RNCamera/CameraRotationHelper.cs b/windows/RNCamera/CameraRotationHelper.cs new file mode 100644 index 000000000..0b34b90ba --- /dev/null +++ b/windows/RNCamera/CameraRotationHelper.cs @@ -0,0 +1,229 @@ +using System; +using Windows.Devices.Enumeration; +using Windows.Devices.Sensors; +using Windows.Graphics.Display; +using Windows.Storage.FileProperties; + +namespace RNCamera +{ + class CameraRotationHelper + { + private EnclosureLocation _cameraEnclosureLocation; + private DisplayInformation _displayInformation = DisplayInformation.GetForCurrentView(); + private SimpleOrientationSensor _orientationSensor = SimpleOrientationSensor.GetDefault(); + + /// + /// Occurs each time the simple orientation sensor reports a new sensor reading or when the display's current or native orientation changes + /// + public event EventHandler OrientationChanged; + + public CameraRotationHelper(EnclosureLocation cameraEnclosureLocation) + { + _cameraEnclosureLocation = cameraEnclosureLocation; + if (!IsEnclosureLocationExternal(_cameraEnclosureLocation) && _orientationSensor != null) + { + _orientationSensor.OrientationChanged += SimpleOrientationSensor_OrientationChanged; + } + _displayInformation.OrientationChanged += DisplayInformation_OrientationChanged; + } + + /// + /// Detects whether or not the camera is external to the device + /// + public static bool IsEnclosureLocationExternal(EnclosureLocation enclosureLocation) + { + return (enclosureLocation == null || enclosureLocation.Panel == Windows.Devices.Enumeration.Panel.Unknown); + } + + /// + /// Gets the rotation of the camera to rotate pictures/videos when saving to file + /// + public SimpleOrientation GetCameraCaptureOrientation() + { + if (IsEnclosureLocationExternal(_cameraEnclosureLocation)) + { + // Cameras that are not attached to the device do not rotate along with it, so apply no rotation + return SimpleOrientation.NotRotated; + } + + // Get the device orientation offset by the camera hardware offset + var deviceOrientation = _orientationSensor?.GetCurrentOrientation() ?? SimpleOrientation.NotRotated; + var result = SubtractOrientations(deviceOrientation, GetCameraOrientationRelativeToNativeOrientation()); + + // If the preview is being mirrored for a front-facing camera, then the rotation should be inverted + if (ShouldMirrorPreview()) + { + result = MirrorOrientation(result); + } + return result; + } + + /// + /// Gets the rotation of the camera to display the camera preview + /// + public SimpleOrientation GetCameraPreviewOrientation() + { + if (IsEnclosureLocationExternal(_cameraEnclosureLocation)) + { + // Cameras that are not attached to the device do not rotate along with it, so apply no rotation + return SimpleOrientation.NotRotated; + } + + // Get the app display rotation offset by the camera hardware offset + var result = ConvertDisplayOrientationToSimpleOrientation(_displayInformation.CurrentOrientation); + result = SubtractOrientations(result, GetCameraOrientationRelativeToNativeOrientation()); + + // If the preview is being mirrored for a front-facing camera, then the rotation should be inverted + if (ShouldMirrorPreview()) + { + result = MirrorOrientation(result); + } + return result; + } + + public static PhotoOrientation ConvertSimpleOrientationToPhotoOrientation(SimpleOrientation orientation) + { + switch (orientation) + { + case SimpleOrientation.Rotated90DegreesCounterclockwise: + return PhotoOrientation.Rotate90; + case SimpleOrientation.Rotated180DegreesCounterclockwise: + return PhotoOrientation.Rotate180; + case SimpleOrientation.Rotated270DegreesCounterclockwise: + return PhotoOrientation.Rotate270; + case SimpleOrientation.NotRotated: + default: + return PhotoOrientation.Normal; + } + } + + public static int ConvertSimpleOrientationToClockwiseDegrees(SimpleOrientation orientation) + { + switch (orientation) + { + case SimpleOrientation.Rotated90DegreesCounterclockwise: + return 270; + case SimpleOrientation.Rotated180DegreesCounterclockwise: + return 180; + case SimpleOrientation.Rotated270DegreesCounterclockwise: + return 90; + case SimpleOrientation.NotRotated: + default: + return 0; + } + } + + private SimpleOrientation ConvertDisplayOrientationToSimpleOrientation(DisplayOrientations orientation) + { + SimpleOrientation result; + switch (orientation) + { + case DisplayOrientations.Landscape: + result = SimpleOrientation.NotRotated; + break; + case DisplayOrientations.PortraitFlipped: + result = SimpleOrientation.Rotated90DegreesCounterclockwise; + break; + case DisplayOrientations.LandscapeFlipped: + result = SimpleOrientation.Rotated180DegreesCounterclockwise; + break; + case DisplayOrientations.Portrait: + default: + result = SimpleOrientation.Rotated270DegreesCounterclockwise; + break; + } + + // Above assumes landscape; offset is needed if native orientation is portrait + if (_displayInformation.NativeOrientation == DisplayOrientations.Portrait) + { + result = AddOrientations(result, SimpleOrientation.Rotated90DegreesCounterclockwise); + } + + return result; + } + + private static SimpleOrientation MirrorOrientation(SimpleOrientation orientation) + { + // This only affects the 90 and 270 degree cases, because rotating 0 and 180 degrees is the same clockwise and counter-clockwise + switch (orientation) + { + case SimpleOrientation.Rotated90DegreesCounterclockwise: + return SimpleOrientation.Rotated270DegreesCounterclockwise; + case SimpleOrientation.Rotated270DegreesCounterclockwise: + return SimpleOrientation.Rotated90DegreesCounterclockwise; + } + return orientation; + } + + private static SimpleOrientation AddOrientations(SimpleOrientation a, SimpleOrientation b) + { + var aRot = ConvertSimpleOrientationToClockwiseDegrees(a); + var bRot = ConvertSimpleOrientationToClockwiseDegrees(b); + var result = (aRot + bRot) % 360; + return ConvertClockwiseDegreesToSimpleOrientation(result); + } + + private static SimpleOrientation SubtractOrientations(SimpleOrientation a, SimpleOrientation b) + { + var aRot = ConvertSimpleOrientationToClockwiseDegrees(a); + var bRot = ConvertSimpleOrientationToClockwiseDegrees(b); + // Add 360 to ensure the modulus operator does not operate on a negative + var result = (360 + (aRot - bRot)) % 360; + return ConvertClockwiseDegreesToSimpleOrientation(result); + } + + private static SimpleOrientation ConvertClockwiseDegreesToSimpleOrientation(int orientation) + { + switch (orientation) + { + case 270: + return SimpleOrientation.Rotated90DegreesCounterclockwise; + case 180: + return SimpleOrientation.Rotated180DegreesCounterclockwise; + case 90: + return SimpleOrientation.Rotated270DegreesCounterclockwise; + case 0: + default: + return SimpleOrientation.NotRotated; + } + } + + private void SimpleOrientationSensor_OrientationChanged(SimpleOrientationSensor sender, SimpleOrientationSensorOrientationChangedEventArgs args) + { + if (args.Orientation != SimpleOrientation.Faceup && args.Orientation != SimpleOrientation.Facedown) + { + // Only raise the OrientationChanged event if the device is not parallel to the ground. This allows users to take pictures of documents (FaceUp) + // or the ceiling (FaceDown) in portrait or landscape, by first holding the device in the desired orientation, and then pointing the camera + // either up or down, at the desired subject. + //Note: This assumes that the camera is either facing the same way as the screen, or the opposite way. For devices with cameras mounted + // on other panels, this logic should be adjusted. + OrientationChanged?.Invoke(this, false); + } + } + + private void DisplayInformation_OrientationChanged(DisplayInformation sender, object args) + { + OrientationChanged?.Invoke(this, true); + } + + private bool ShouldMirrorPreview() + { + // It is recommended that applications mirror the preview for front-facing cameras, as it gives users a more natural experience, since it behaves more like a mirror + return (_cameraEnclosureLocation.Panel == Windows.Devices.Enumeration.Panel.Front); + } + + private SimpleOrientation GetCameraOrientationRelativeToNativeOrientation() + { + // Get the rotation angle of the camera enclosure as it is mounted in the device hardware + var enclosureAngle = ConvertClockwiseDegreesToSimpleOrientation((int)_cameraEnclosureLocation.RotationAngleInDegreesClockwise); + + // Account for the fact that, on portrait-first devices, the built in camera sensor is read at a 90 degree offset to the native orientation + if (_displayInformation.NativeOrientation == DisplayOrientations.Portrait && !IsEnclosureLocationExternal(_cameraEnclosureLocation)) + { + enclosureAngle = AddOrientations(SimpleOrientation.Rotated90DegreesCounterclockwise, enclosureAngle); + } + + return enclosureAngle; + } + } +} \ No newline at end of file diff --git a/windows/RNCamera/GeolocationHelper.cs b/windows/RNCamera/GeolocationHelper.cs new file mode 100644 index 000000000..9905696a4 --- /dev/null +++ b/windows/RNCamera/GeolocationHelper.cs @@ -0,0 +1,72 @@ +using System; +using Windows.Foundation.Collections; + +namespace RNCamera +{ + static class GeolocationHelper + { + public static PropertySet GetLatitudeProperties(double latitude) + { + // Latitude and longitude are returned as double precision numbers, + // but we want to convert to degrees/minutes/seconds format. + var latRefText = (latitude >= 0) ? "N" : "S"; + var latDeg = Math.Floor(Math.Abs(latitude)); + var latMin = Math.Floor((Math.Abs(latitude) - latDeg) * 60); + var latSec = (Math.Abs(latitude) - latDeg - latMin / 60) * 3600; + + uint[] latitudeNumerator = + { + (uint)latDeg, + (uint)latMin, + (uint)(latSec * 10000) + }; + + uint[] denominator = + { + 1, + 1, + 10000 + }; + + return new PropertySet + { + { "System.GPS.LatitudeRef", latRefText }, + { "System.GPS.LatitudeNumerator", latitudeNumerator }, + { "System.GPS.LatitudeDenominator", denominator }, + }; + } + + public static PropertySet GetLongitudeProperties(double longitude) + { + // Latitude and longitude are returned as double precision numbers, + // but we want to convert to degrees/minutes/seconds format. + var longRefText = (longitude >= 0) ? "E" : "W"; + var longDeg = Math.Floor(Math.Abs(longitude)); + var longMin = Math.Floor((Math.Abs(longitude) - longDeg) * 60); + var longSec = (Math.Abs(longitude) - longDeg - longMin / 60) * 3600; + + uint[] longitudeNumerator = + { + (uint)longDeg, + (uint)longMin, + (uint)(longSec * 10000) + }; + + uint[] denominator = + { + 1, + 1, + 10000 + }; + + var pset = new PropertySet(); + + return new PropertySet + { + { "System.GPS.LongitudeRef", longRefText }, + { "System.GPS.LongitudeNumerator", longitudeNumerator }, + { "System.GPS.LongitudeDenominator", denominator }, + }; + } + } +} diff --git a/windows/RNCamera/JObjectExtensions.cs b/windows/RNCamera/JObjectExtensions.cs new file mode 100644 index 000000000..b25ce2cbe --- /dev/null +++ b/windows/RNCamera/JObjectExtensions.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json.Linq; +using System.Collections.Generic; + +namespace RNCamera +{ + static class JObjectExtensions + { + public static bool ContainsKey(this JObject json, string key) + { + return ((IDictionary)json).ContainsKey(key); + } + } +} diff --git a/windows/RNCamera/MediaCaptureExtensions.cs b/windows/RNCamera/MediaCaptureExtensions.cs new file mode 100644 index 000000000..84f29a4cc --- /dev/null +++ b/windows/RNCamera/MediaCaptureExtensions.cs @@ -0,0 +1,26 @@ +using Windows.Media.Capture; + +namespace RNCamera +{ + static class MediaCaptureExtensions + { + public static void SetTorchMode(this MediaCapture mediaCapture, int torchMode) + { + var torchControl = mediaCapture.VideoDeviceController.TorchControl; + if (torchControl.Supported) + { + torchControl.Enabled = torchMode == RCTCameraModule.CameraTorchModeOn; + } + } + + public static void SetFlashMode(this MediaCapture mediaCapture, int flashMode) + { + var flashControl = mediaCapture.VideoDeviceController.FlashControl; + if (flashControl.Supported) + { + flashControl.Enabled = flashMode == RCTCameraModule.CameraFlashModeOn; + flashControl.Auto = flashMode == RCTCameraModule.CameraFlashModeAuto; + } + } + } +} diff --git a/windows/RNCamera/Properties/AssemblyInfo.cs b/windows/RNCamera/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..06a344f3c --- /dev/null +++ b/windows/RNCamera/Properties/AssemblyInfo.cs @@ -0,0 +1,30 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("RNCamera")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("RNCamera")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: ComVisible(false)] + \ No newline at end of file diff --git a/windows/RNCamera/Properties/RNCamera.rd.xml b/windows/RNCamera/Properties/RNCamera.rd.xml new file mode 100644 index 000000000..cb5f4a084 --- /dev/null +++ b/windows/RNCamera/Properties/RNCamera.rd.xml @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/windows/RNCamera/RCTCameraModule.cs b/windows/RNCamera/RCTCameraModule.cs new file mode 100644 index 000000000..269bd0bc7 --- /dev/null +++ b/windows/RNCamera/RCTCameraModule.cs @@ -0,0 +1,473 @@ +using Newtonsoft.Json.Linq; +using ReactNative.Bridge; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Windows.Devices.Enumeration; +using Windows.Devices.Sensors; +using Windows.Foundation; +using Windows.Graphics.Imaging; +using Windows.Media.Capture; +using Windows.Media.MediaProperties; +using Windows.Storage; +using Windows.Storage.FileProperties; +using Windows.Storage.Streams; +using ZXing; +using static System.FormattableString; + +namespace RNCamera +{ + class RCTCameraModule : ReactContextNativeModuleBase, ILifecycleEventListener + { + public const int CameraAspectFill = 0; + public const int CameraAspectFit = 1; + public const int CameraAspectStretch = 2; + public const int CameraCaptureModeStill = 0; + public const int CameraCaptureModeVideo = 1; + public const int CameraCaptureTargetMemory = 0; + public const int CameraCaptureTargetDisk = 1; + public const int CameraCaptureTargetCameraRoll = 2; + public const int CameraCaptureTargetTemp = 3; + public const int CameraOrientationAuto = int.MaxValue; + public const int CameraOrientationPortrait = (int)SimpleOrientation.NotRotated; + public const int CameraOrientationPortraitUpsideDown = (int)SimpleOrientation.Rotated180DegreesCounterclockwise; + public const int CameraOrientationLandscapeLeft = (int)SimpleOrientation.Rotated90DegreesCounterclockwise; + public const int CameraOrientationLandscapeRight = (int)SimpleOrientation.Rotated270DegreesCounterclockwise; + public const int CameraTypeFront = (int)Panel.Front; + public const int CameraTypeBack = (int)Panel.Back; + public const int CameraFlashModeOff = 0; + public const int CameraFlashModeOn = 1; + public const int CameraFlashModeAuto = 2; + public const int CameraTorchModeOff = 0; + public const int CameraTorchModeOn = 1; + public const int CameraTorchModeAuto = 2; + public const int CameraCaptureQualityHigh = (int)VideoEncodingQuality.HD1080p; + public const int CameraCaptureQualityLow = (int)VideoEncodingQuality.HD720p; + public const int CameraCaptureQuality1080p = (int)VideoEncodingQuality.HD1080p; + public const int CameraCaptureQuality720p = (int)VideoEncodingQuality.HD720p; + public const int MediaTypeImage = 1; + public const int MediaTypeVideo = 2; + + private static readonly Guid RotationKey = new Guid("C380465D-2271-428C-9B83-ECEA3B4A85C1"); + + private Task _recordingTask; + private CancellationTokenSource _recordingCancellation; + + public RCTCameraModule(ReactContext reactContext) + : base(reactContext) + { + } + + public override string Name + { + get + { + return "CameraModule"; + } + } + + public override IReadOnlyDictionary Constants + { + get + { + return new Dictionary + { + { "Aspect", GetAspectConstants() }, + { "BarCodeType", GetBarcodeConstants() }, + { "Type", GetTypeConstants() }, + { "CaptureQuality", GetCaptureQualityConstants() }, + { "CaptureMode", GetCaptureModeConstants() }, + { "CaptureTarget", GetCaptureTargetConstants() }, + { "Orientation", GetOrientationConstants() }, + { "FlashMode", GetFlashModeConstants() }, + { "TorchMode", GetTorchModeConstants() }, + }; + } + } + + public CameraForViewManager CameraManager { get; } = new CameraForViewManager(); + + [ReactMethod] + public async void capture(JObject options, IPromise promise) + { + var viewTag = options.Value("view"); + var cameraForView = CameraManager.GetCameraForView(viewTag); + if (cameraForView == null) + { + promise.Reject("No camera found."); + return; + } + + var mode = options.Value("mode"); + if (mode == CameraCaptureModeVideo) + { + if (_recordingTask != null) + { + promise.Reject("Cannot run more than one capture operation."); + return; + } + + _recordingCancellation = new CancellationTokenSource(); + _recordingTask = RecordAsync(cameraForView, options, promise, _recordingCancellation.Token); + return; + } + else + { + await CapturePhotoAsync(cameraForView, options, promise).ConfigureAwait(false); + } + } + + [ReactMethod] + public async void stopCapture(IPromise promise) + { + var recordingTask = _recordingTask; + if (recordingTask != null) + { + _recordingCancellation.Cancel(); + await recordingTask; + promise.Resolve("Finished recording."); + } + else + { + promise.Resolve("Not recording."); + } + } + + [ReactMethod] + public void hasFlash(JObject options, IPromise promise) + { + var viewTag = options.Value("view"); + var mediaCapture = CameraManager.GetCameraForView(viewTag).MediaCapture; + if (mediaCapture == null) + { + promise.Reject("No camera found."); + return; + } + + var isSupported = mediaCapture.VideoDeviceController.FlashControl.Supported; + promise.Resolve(isSupported); + } + + public void OnSuspend() + { + var recordingTask = _recordingTask; + if (recordingTask != null) + { + _recordingCancellation.Cancel(); + recordingTask.Wait(); + } + } + + public void OnResume() + { + } + + public void OnDestroy() + { + } + + private async Task CapturePhotoAsync(CameraForView cameraForView, JObject options, IPromise promise) + { + var mediaCapture = cameraForView.MediaCapture; + var encoding = ImageEncodingProperties.CreateJpeg(); + var target = options.Value("target"); + using (var stream = new InMemoryRandomAccessStream()) + { + await mediaCapture.CapturePhotoToStreamAsync(encoding, stream).AsTask().ConfigureAwait(false); + if (target == CameraCaptureTargetMemory) + { + var data = await GetBase64DataAsync(stream).ConfigureAwait(false); + promise.Resolve(new JObject + { + { "data", data }, + }); + } + else + { + var storageFile = await GetOutputStorageFileAsync(MediaTypeImage, target).AsTask().ConfigureAwait(false); + var orientation = await cameraForView.GetCameraCaptureOrientationAsync().ConfigureAwait(false); + var photoOrientation = CameraRotationHelper.ConvertSimpleOrientationToPhotoOrientation(orientation); + await ReencodeAndSavePhotoAsync(stream, storageFile, photoOrientation).ConfigureAwait(false); +; await UpdateImagePropertiesAsync(storageFile, options).ConfigureAwait(false); + promise.Resolve(new JObject + { + { "path", storageFile.Path }, + }); + } + } + } + + private async Task RecordAsync(CameraForView cameraForView, JObject options, IPromise promise, CancellationToken token) + { + var mediaCapture = cameraForView.MediaCapture; + var taskCompletionSource = new TaskCompletionSource(); + using (var cancellationTokenSource = new CancellationTokenSource()) + using (token.Register(cancellationTokenSource.Cancel)) + using (cancellationTokenSource.Token.Register(async () => await StopRecordingAsync(mediaCapture, taskCompletionSource))) + { + var quality = (VideoEncodingQuality)options.Value("quality"); + var encodingProfile = MediaEncodingProfile.CreateMp4(quality); + + mediaCapture.AudioDeviceController.Muted = options.Value("audio"); + + var orientation = await cameraForView.GetCameraCaptureOrientationAsync().ConfigureAwait(false); + encodingProfile.Video.Properties.Add(RotationKey, CameraRotationHelper.ConvertSimpleOrientationToClockwiseDegrees(orientation)); + + var stream = default(InMemoryRandomAccessStream); + + try + { + var target = options.Value("target"); + var outputFile = default(StorageFile); + if (target == CameraCaptureTargetMemory) + { + stream = new InMemoryRandomAccessStream(); + await mediaCapture.StartRecordToStreamAsync(encodingProfile, stream).AsTask().ConfigureAwait(false); + } + else + { + outputFile = await GetOutputStorageFileAsync(MediaTypeVideo, target).AsTask().ConfigureAwait(false); + await mediaCapture.StartRecordToStorageFileAsync(encodingProfile, outputFile).AsTask().ConfigureAwait(false); + } + + if (options.ContainsKey("totalSeconds")) + { + var totalSeconds = options.Value("totalSeconds"); + if (totalSeconds > 0) + { + cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(totalSeconds)); + } + } + + await taskCompletionSource.Task.ConfigureAwait(false); + + if (target == CameraCaptureTargetMemory) + { + var data = await GetBase64DataAsync(stream).ConfigureAwait(false); + promise.Resolve(new JObject + { + { "data", data }, + }); + } + else + { + await UpdateVideoPropertiesAsync(outputFile, options).ConfigureAwait(false); + promise.Resolve(new JObject + { + { "path", outputFile.Path }, + }); + } + } + finally + { + stream?.Dispose(); + } + } + + _recordingCancellation.Dispose(); + _recordingTask = null; + } + + private async Task StopRecordingAsync(MediaCapture mediaCapture, TaskCompletionSource taskCompletionSource) + { + await mediaCapture.StopRecordAsync().AsTask().ConfigureAwait(false); + taskCompletionSource.SetResult(true); + } + + private static async Task GetBase64DataAsync(IRandomAccessStream stream) + { + var size = (int)stream.Size; + var data = new byte[size]; + using (var readableStream = stream.AsStreamForRead()) + { + await readableStream.ReadAsync(data, 0, size).ConfigureAwait(false); + return Convert.ToBase64String(data); + } + } + + private static async Task UpdateVideoPropertiesAsync(StorageFile storageFile, JObject options) + { + var props = await storageFile.Properties.GetVideoPropertiesAsync().AsTask().ConfigureAwait(false); + if (options.ContainsKey("title")) + { + props.Title = options.Value("title"); + } + + if (options.ContainsKey("latitude")) + { + await props.SavePropertiesAsync(GeolocationHelper.GetLatitudeProperties(options.Value("latitude"))).AsTask().ConfigureAwait(false); + } + + if (options.ContainsKey("longitude")) + { + await props.SavePropertiesAsync(GeolocationHelper.GetLongitudeProperties(options.Value("longitude"))).AsTask().ConfigureAwait(false); + } + } + + private static async Task UpdateImagePropertiesAsync(StorageFile storageFile, JObject options) + { + var props = await storageFile.Properties.GetImagePropertiesAsync().AsTask().ConfigureAwait(false); + if (options.ContainsKey("title")) + { + props.Title = options.Value("title"); + } + + if (options.ContainsKey("latitude")) + { + await props.SavePropertiesAsync(GeolocationHelper.GetLatitudeProperties(options.Value("latitude"))).AsTask().ConfigureAwait(false); + } + + if (options.ContainsKey("longitude")) + { + await props.SavePropertiesAsync(GeolocationHelper.GetLongitudeProperties(options.Value("longitude"))).AsTask().ConfigureAwait(false); + } + } + + private static IAsyncOperation GetOutputStorageFileAsync(int type, int target) + { + var ext = type == MediaTypeImage ? ".jpg" : ".mp4"; + var filename = DateTimeOffset.Now.ToString("yyyyMMdd_HHmmss") + ext; + switch (target) + { + case CameraCaptureTargetMemory: + case CameraCaptureTargetTemp: + return ApplicationData.Current.TemporaryFolder.CreateFileAsync(filename); + case CameraCaptureTargetCameraRoll: + return KnownFolders.CameraRoll.CreateFileAsync(filename); + case CameraCaptureTargetDisk: + if (type == MediaTypeImage) + { + return KnownFolders.PicturesLibrary.CreateFileAsync(filename); + } + else + { + return KnownFolders.VideosLibrary.CreateFileAsync(filename); + } + default: + throw new InvalidOperationException( + Invariant($"Unknown capture target '{target}'.")); + } + } + + private static async Task ReencodeAndSavePhotoAsync(IRandomAccessStream stream, StorageFile file, PhotoOrientation photoOrientation) + { + using (var inputStream = stream) + { + var decoder = await BitmapDecoder.CreateAsync(inputStream).AsTask().ConfigureAwait(false); + + using (var outputStream = await file.OpenAsync(FileAccessMode.ReadWrite).AsTask().ConfigureAwait(false)) + { + var encoder = await BitmapEncoder.CreateForTranscodingAsync(outputStream, decoder).AsTask().ConfigureAwait(false); + + var properties = new BitmapPropertySet { { "System.Photo.Orientation", new BitmapTypedValue(photoOrientation, PropertyType.UInt16) } }; + + await encoder.BitmapProperties.SetPropertiesAsync(properties).AsTask().ConfigureAwait(false); + await encoder.FlushAsync().AsTask().ConfigureAwait(false); + } + } + } + + private static IReadOnlyDictionary GetAspectConstants() + { + return new Dictionary + { + { "stretch", CameraAspectStretch }, + { "fit", CameraAspectFit }, + { "fill", CameraAspectFill }, + }; + } + + private static IReadOnlyDictionary GetBarcodeConstants() + { + // TODO: code39mod43, itf14, etc. + return new Dictionary + { + { BarcodeFormat.UPC_E.GetName(), BarcodeFormat.UPC_E }, + { BarcodeFormat.CODE_39.GetName(), BarcodeFormat.CODE_39 }, + { BarcodeFormat.EAN_13.GetName(), BarcodeFormat.EAN_13 }, + { BarcodeFormat.EAN_8.GetName(), BarcodeFormat.EAN_8 }, + { BarcodeFormat.CODE_93.GetName(), BarcodeFormat.CODE_93 }, + { BarcodeFormat.CODE_128.GetName(), BarcodeFormat.CODE_128 }, + { BarcodeFormat.PDF_417.GetName(), BarcodeFormat.PDF_417 }, + { BarcodeFormat.QR_CODE.GetName(), BarcodeFormat.QR_CODE }, + { BarcodeFormat.AZTEC.GetName(), BarcodeFormat.AZTEC }, + { BarcodeFormat.ITF.GetName(), BarcodeFormat.ITF }, + { BarcodeFormat.DATA_MATRIX.GetName(), BarcodeFormat.DATA_MATRIX }, + }; + } + + private static IReadOnlyDictionary GetTypeConstants() + { + return new Dictionary + { + { "front", CameraTypeFront }, + { "back", CameraTypeBack }, + }; + } + + private static IReadOnlyDictionary GetCaptureQualityConstants() + { + return new Dictionary + { + { "low", CameraCaptureQualityLow }, + { "high", CameraCaptureQualityHigh }, + { "720p", CameraCaptureQuality720p }, + { "1080p", CameraCaptureQuality1080p }, + }; + } + + private static IReadOnlyDictionary GetCaptureModeConstants() + { + return new Dictionary + { + { "still", CameraCaptureModeStill }, + { "video", CameraCaptureModeVideo }, + }; + } + + private static IReadOnlyDictionary GetCaptureTargetConstants() + { + return new Dictionary + { + { "memory", CameraCaptureTargetMemory }, + { "disk", CameraCaptureTargetDisk }, + { "cameraRoll", CameraCaptureTargetCameraRoll }, + { "temp", CameraCaptureTargetTemp }, + }; + } + + private static IReadOnlyDictionary GetOrientationConstants() + { + return new Dictionary + { + { "auto", CameraOrientationAuto }, + { "landscapeLeft", CameraOrientationLandscapeLeft }, + { "landscapeRight", CameraOrientationLandscapeRight }, + { "portrait", CameraOrientationPortrait }, + { "portraitUpsideDown", CameraOrientationPortraitUpsideDown }, + }; + } + + private static IReadOnlyDictionary GetFlashModeConstants() + { + return new Dictionary + { + { "off", CameraFlashModeOff }, + { "on", CameraFlashModeOn }, + { "auto", CameraOrientationAuto }, + }; + } + + private static IReadOnlyDictionary GetTorchModeConstants() + { + return new Dictionary + { + { "off", CameraTorchModeOff }, + { "on", CameraTorchModeOn }, + { "auto", CameraTorchModeAuto }, + }; + } + } +} diff --git a/windows/RNCamera/RCTCameraViewManager.cs b/windows/RNCamera/RCTCameraViewManager.cs new file mode 100644 index 000000000..5ecdc8530 --- /dev/null +++ b/windows/RNCamera/RCTCameraViewManager.cs @@ -0,0 +1,188 @@ +using Newtonsoft.Json.Linq; +using ReactNative.Bridge; +using ReactNative.Modules.Core; +using ReactNative.UIManager; +using ReactNative.UIManager.Annotations; +using System.Linq; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Media; +using ZXing; + +namespace RNCamera +{ + class RCTCameraViewManager : SimpleViewManager + { + private readonly ReactContext _reactContext; + + public RCTCameraViewManager(ReactContext reactContext) + { + _reactContext = reactContext; + } + + public override string Name + { + get + { + return "RCTCamera"; + } + } + + public CameraForViewManager CameraManager + { + get + { + return _reactContext.GetNativeModule().CameraManager; + } + } + + [ReactProp("aspect")] + public void SetAspect(CaptureElement view, int aspect) + { + switch (aspect) + { + case RCTCameraModule.CameraAspectFill: + view.Stretch = Stretch.Uniform; + break; + case RCTCameraModule.CameraAspectFit: + view.Stretch = Stretch.UniformToFill; + break; + case RCTCameraModule.CameraAspectStretch: + view.Stretch = Stretch.Fill; + break; + default: + view.Stretch = Stretch.None; + break; + } + } + + [ReactProp("captureTarget")] + public void SetCaptureTarget(CaptureElement view, int captureTarget) + { + // No reason to handle this props valeu here since it's passed to the `capture` method. + } + + [ReactProp("type")] + public async void SetType(CaptureElement view, int type) + { + var camera = CameraManager.GetOrCreateCameraForView(view); + await camera.UpdatePanelAsync((Windows.Devices.Enumeration.Panel)type); + } + + [ReactProp("captureQuality")] + public void SetCaptureQuality(CaptureElement view, int captureQuality) + { + // No reason to handle this props valeu here since it's passed to the `capture` method. + } + + [ReactProp("torchMode")] + public void SetTorchMode(CaptureElement view, int torchMode) + { + var camera = CameraManager.GetOrCreateCameraForView(view); + camera.TorchMode = torchMode; + } + + [ReactProp("flashMode")] + public void SetFlashMode(CaptureElement view, int flashMode) + { + var camera = CameraManager.GetOrCreateCameraForView(view); + camera.FlashMode = flashMode; + } + + [ReactProp("orientation")] + public void SetOrientation(CaptureElement view, int orientation) + { + var camera = CameraManager.GetOrCreateCameraForView(view); + camera.Orientation = orientation; + } + + [ReactProp("barcodeScannerEnabled")] + public void SetBarcodeScannerEnabled(CaptureElement view, bool enabled) + { + var camera = CameraManager.GetOrCreateCameraForView(view); + var wasEnabled = camera.BarcodeScanningEnabled; + camera.BarcodeScanningEnabled = enabled; + if (enabled) + { + camera.BarcodeScanned += OnBarcodeScanned; + } + else if (wasEnabled) + { + camera.BarcodeScanned -= OnBarcodeScanned; + } + } + + [ReactProp("barCodeTypes")] + public void SetBarcodeTypes(CaptureElement view, JArray barcodeTypes) + { + var camera = CameraManager.GetOrCreateCameraForView(view); + var barcodeReader = camera.BarcodeReader; + if (barcodeReader == null) + { + barcodeReader = new BarcodeReader(); + camera.BarcodeReader = barcodeReader; + } + + barcodeReader.Options.PossibleFormats = + barcodeTypes.Select(t => (BarcodeFormat)t.Value()).ToList(); + } + + [ReactProp("keepAwake")] + public void SetKeepAwake(CaptureElement view, bool keepAwake) + { + var camera = CameraManager.GetOrCreateCameraForView(view); + camera.KeepAwake = keepAwake; + } + + public override async void OnDropViewInstance(ThemedReactContext reactContext, CaptureElement view) + { + var viewTag = view.GetTag(); + var camera = CameraManager.GetCameraForView(viewTag); + + if (camera.BarcodeScanningEnabled) + { + camera.BarcodeScanned -= OnBarcodeScanned; + } + + await camera.DisposeAsync().ConfigureAwait(false); + + CameraManager.DropCameraForView(view); + } + + protected override async void OnAfterUpdateTransaction(CaptureElement view) + { + var camera = CameraManager.GetCameraForView(view.GetTag()); + if (!camera.IsInitialized) + { + await camera.InitializeAsync().ConfigureAwait(false); + } + } + + protected override CaptureElement CreateViewInstance(ThemedReactContext reactContext) + { + return new CaptureElement(); + } + + private void OnBarcodeScanned(Result result) + { + var resultPoints = new JArray(); + foreach (var point in result.ResultPoints) + { + resultPoints.Add(new JObject + { + { "x", point.X }, + { "y", point.Y }, + }); + } + + var eventData = new JObject + { + { "bounds", resultPoints }, + { "text", result.Text }, + { "type", result.BarcodeFormat.GetName() }, + }; + + _reactContext.GetJavaScriptModule() + .emit("CameraBarCodeReadWindows", eventData); + } + } +} diff --git a/windows/RNCamera/RNCamera.csproj b/windows/RNCamera/RNCamera.csproj new file mode 100644 index 000000000..bfe4ea60e --- /dev/null +++ b/windows/RNCamera/RNCamera.csproj @@ -0,0 +1,174 @@ + + + + + Debug + x86 + {F11038F0-88E5-11E7-BA7C-E36C7490591A} + Library + Properties + RNCamera + RNCamera + en-US + UAP + 10.0.14393.0 + 10.0.10240.0 + 14 + 512 + {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + ..\..\node_modules + + + ..\.. + + + x86 + true + bin\x86\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x86 + false + prompt + + + x86 + bin\x86\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x86 + false + prompt + + + ARM + true + bin\ARM\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + ARM + false + prompt + + + ARM + bin\ARM\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + ARM + false + prompt + + + x64 + true + bin\x64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x64 + false + prompt + + + x64 + bin\x64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x64 + false + prompt + + + true + bin\x86\Development\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + true + full + x86 + false + prompt + MinimumRecommendedRules.ruleset + + + true + bin\ARM\Development\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + true + full + ARM + false + prompt + MinimumRecommendedRules.ruleset + + + true + bin\x64\Development\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + true + full + x64 + false + prompt + MinimumRecommendedRules.ruleset + + + PackageReference + + + + + + + + + + + + + + + + + + {c7673ad5-e3aa-468c-a5fd-fa38154e205c} + ReactNative + + + + + 6.0.6 + + + 10.0.3 + + + 4.0.0-preview00001 + + + 0.16.0 + + + + 14.0 + + + + \ No newline at end of file diff --git a/windows/RNCamera/RNCameraPackage.cs b/windows/RNCamera/RNCameraPackage.cs new file mode 100644 index 000000000..01e5f01b3 --- /dev/null +++ b/windows/RNCamera/RNCameraPackage.cs @@ -0,0 +1,56 @@ +using ReactNative.Bridge; +using ReactNative.Modules.Core; +using ReactNative.UIManager; +using System; +using System.Collections.Generic; + +namespace RNCamera +{ + /// + /// Package defining core framework modules (e.g., ). + /// It should be used for modules that require special integration with + /// other framework parts (e.g., with the list of packages to load view + /// managers from). + /// + public class RNCameraPackage : IReactPackage + { + /// + /// Creates the list of native modules to register with the react + /// instance. + /// + /// The react application context. + /// The list of native modules. + public IReadOnlyList CreateNativeModules(ReactContext reactContext) + { + return new List + { + new RCTCameraModule(reactContext), + }; + } + + /// + /// Creates the list of JavaScript modules to register with the + /// react instance. + /// + /// The list of JavaScript modules. + public IReadOnlyList CreateJavaScriptModulesConfig() + { + return new List(0); + } + + /// + /// Creates the list of view managers that should be registered with + /// the . + /// + /// The react application context. + /// The list of view managers. + public IReadOnlyList CreateViewManagers( + ReactContext reactContext) + { + return new List + { + new RCTCameraViewManager(reactContext), + }; + } + } +}