react-native-camera/windows/RNCamera/CameraForView.cs
Eric Rozell 1c627490c7 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
2018-12-03 13:50:37 -02:00

397 lines
13 KiB
C#

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<Result> 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<SimpleOrientation> GetCameraCaptureOrientationAsync()
{
var result = new TaskCompletionSource<SimpleOrientation>();
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<Result> GetBarcodeScanningObservable()
{
return Observable.Create(
new Func<IObserver<Result>, CancellationToken, Task>(DoBarcodeScanningAsync));
}
private async Task DoBarcodeScanningAsync(IObserver<Result> 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);
}
}
}