485 lines
19 KiB
C#
485 lines
19 KiB
C#
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 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 "RNCameraModule";
|
|
}
|
|
}
|
|
|
|
public override IReadOnlyDictionary<string, object> Constants
|
|
{
|
|
get
|
|
{
|
|
return new Dictionary<string, object>
|
|
{
|
|
{ "Aspect", GetAspectConstants() },
|
|
{ "BarCodeType", GetBarcodeConstants() },
|
|
{ "Type", GetTypeConstants() },
|
|
{ "VideoQuality", GetCaptureQualityConstants() },
|
|
{ "CaptureTarget", GetCaptureTargetConstants() },
|
|
{ "Orientation", GetOrientationConstants() },
|
|
{ "FlashMode", GetFlashModeConstants() },
|
|
{ "TorchMode", GetTorchModeConstants() },
|
|
};
|
|
}
|
|
}
|
|
|
|
public CameraForViewManager CameraManager { get; } = new CameraForViewManager();
|
|
|
|
[ReactMethod]
|
|
public async void checkMediaCapturePermission(IPromise promise)
|
|
{
|
|
MediaCapture newCapture = new MediaCapture();
|
|
try
|
|
{
|
|
await newCapture.InitializeAsync();
|
|
}
|
|
catch
|
|
{
|
|
promise.Resolve(false);
|
|
return;
|
|
}
|
|
|
|
promise.Resolve(true);
|
|
}
|
|
|
|
[ReactMethod]
|
|
public async void takePicture(JObject options, int viewTag, IPromise promise)
|
|
{
|
|
//var viewTag = options.Value<int>("view");
|
|
var cameraForView = CameraManager.GetCameraForView(viewTag);
|
|
if (cameraForView == null)
|
|
{
|
|
promise.Reject("No camera found.");
|
|
return;
|
|
}
|
|
|
|
await CapturePhotoAsync(cameraForView, options, promise).ConfigureAwait(false);
|
|
}
|
|
|
|
[ReactMethod]
|
|
public void record(JObject options, int viewTag, IPromise promise)
|
|
{
|
|
//var viewTag = options.Value<int>("view");
|
|
var cameraForView = CameraManager.GetCameraForView(viewTag);
|
|
if (cameraForView == null)
|
|
{
|
|
promise.Reject("No camera found.");
|
|
return;
|
|
}
|
|
|
|
if (_recordingTask != null)
|
|
{
|
|
promise.Reject("Cannot run more than one capture operation.");
|
|
return;
|
|
}
|
|
|
|
_recordingCancellation = new CancellationTokenSource();
|
|
_recordingTask = RecordAsync(cameraForView, options, promise, _recordingCancellation.Token);
|
|
}
|
|
|
|
|
|
[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<int>("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<int>("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<bool>();
|
|
using (var cancellationTokenSource = new CancellationTokenSource())
|
|
using (token.Register(cancellationTokenSource.Cancel))
|
|
using (cancellationTokenSource.Token.Register(async () => await StopRecordingAsync(mediaCapture, taskCompletionSource)))
|
|
{
|
|
var quality = (VideoEncodingQuality)options.Value<int>("quality");
|
|
var encodingProfile = MediaEncodingProfile.CreateMp4(quality);
|
|
|
|
mediaCapture.AudioDeviceController.Muted = options.Value<bool>("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<int>("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<double>("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<bool> taskCompletionSource)
|
|
{
|
|
await mediaCapture.StopRecordAsync().AsTask().ConfigureAwait(false);
|
|
taskCompletionSource.SetResult(true);
|
|
}
|
|
|
|
private static async Task<string> 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<string>("title");
|
|
}
|
|
|
|
if (options.ContainsKey("latitude"))
|
|
{
|
|
await props.SavePropertiesAsync(GeolocationHelper.GetLatitudeProperties(options.Value<double>("latitude"))).AsTask().ConfigureAwait(false);
|
|
}
|
|
|
|
if (options.ContainsKey("longitude"))
|
|
{
|
|
await props.SavePropertiesAsync(GeolocationHelper.GetLongitudeProperties(options.Value<double>("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<string>("title");
|
|
}
|
|
|
|
if (options.ContainsKey("latitude"))
|
|
{
|
|
await props.SavePropertiesAsync(GeolocationHelper.GetLatitudeProperties(options.Value<double>("latitude"))).AsTask().ConfigureAwait(false);
|
|
}
|
|
|
|
if (options.ContainsKey("longitude"))
|
|
{
|
|
await props.SavePropertiesAsync(GeolocationHelper.GetLongitudeProperties(options.Value<double>("longitude"))).AsTask().ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
private static IAsyncOperation<StorageFile> 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<string, object> GetAspectConstants()
|
|
{
|
|
return new Dictionary<string, object>
|
|
{
|
|
{ "stretch", CameraAspectStretch },
|
|
{ "fit", CameraAspectFit },
|
|
{ "fill", CameraAspectFill },
|
|
};
|
|
}
|
|
|
|
private static IReadOnlyDictionary<string, object> GetBarcodeConstants()
|
|
{
|
|
// TODO: code39mod43, itf14, etc.
|
|
return new Dictionary<string, object>
|
|
{
|
|
{ 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<string, object> GetTypeConstants()
|
|
{
|
|
return new Dictionary<string, object>
|
|
{
|
|
{ "front", CameraTypeFront },
|
|
{ "back", CameraTypeBack },
|
|
};
|
|
}
|
|
|
|
private static IReadOnlyDictionary<string, object> GetCaptureQualityConstants()
|
|
{
|
|
return new Dictionary<string, object>
|
|
{
|
|
{ "low", CameraCaptureQualityLow },
|
|
{ "high", CameraCaptureQualityHigh },
|
|
{ "720p", CameraCaptureQuality720p },
|
|
{ "1080p", CameraCaptureQuality1080p },
|
|
};
|
|
}
|
|
|
|
private static IReadOnlyDictionary<string, object> GetCaptureTargetConstants()
|
|
{
|
|
return new Dictionary<string, object>
|
|
{
|
|
{ "memory", CameraCaptureTargetMemory },
|
|
{ "disk", CameraCaptureTargetDisk },
|
|
{ "cameraRoll", CameraCaptureTargetCameraRoll },
|
|
{ "temp", CameraCaptureTargetTemp },
|
|
};
|
|
}
|
|
|
|
private static IReadOnlyDictionary<string, object> GetOrientationConstants()
|
|
{
|
|
return new Dictionary<string, object>
|
|
{
|
|
{ "auto", CameraOrientationAuto },
|
|
{ "landscapeLeft", CameraOrientationLandscapeLeft },
|
|
{ "landscapeRight", CameraOrientationLandscapeRight },
|
|
{ "portrait", CameraOrientationPortrait },
|
|
{ "portraitUpsideDown", CameraOrientationPortraitUpsideDown },
|
|
};
|
|
}
|
|
|
|
private static IReadOnlyDictionary<string, object> GetFlashModeConstants()
|
|
{
|
|
return new Dictionary<string, object>
|
|
{
|
|
{ "off", CameraFlashModeOff },
|
|
{ "on", CameraFlashModeOn },
|
|
{ "auto", CameraOrientationAuto },
|
|
};
|
|
}
|
|
|
|
private static IReadOnlyDictionary<string, object> GetTorchModeConstants()
|
|
{
|
|
return new Dictionary<string, object>
|
|
{
|
|
{ "off", CameraTorchModeOff },
|
|
{ "on", CameraTorchModeOn },
|
|
{ "auto", CameraTorchModeAuto },
|
|
};
|
|
}
|
|
}
|
|
}
|