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), + }; + } + } +}