using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Devices.Enumeration;
using Windows.Devices.Sensors;
using Windows.Foundation;
using Windows.Foundation.Metadata;
using Windows.Graphics.Display;
using Windows.Graphics.Imaging;
using Windows.Media;
using Windows.Media.Core;
using Windows.Media.Capture;
using Windows.Media.FaceAnalysis;
using Windows.Media.MediaProperties;
using Windows.Phone.UI.Input;
using Windows.Storage;
using Windows.Storage.FileProperties;
using Windows.Storage.Streams;
using Windows.System.Display;
using Windows.UI;
using Windows.UI.Core;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
using Windows.UI.Xaml.Shapes;
namespace TestProject
{
/// <summary>
/// 메인 페이지
/// </summary>
public sealed partial class MainPage : Page
{
//////////////////////////////////////////////////////////////////////////////////////////////////// Field
////////////////////////////////////////////////////////////////////////////////////////// Static
//////////////////////////////////////////////////////////////////////////////// Private
#region Field
/// <summary>
/// 회전 키 GUID
/// </summary>
private static readonly Guid _rotationKeyGUID = new Guid("C380465D-2271-428C-9B83-ECEA3B4A85C1");
#endregion
////////////////////////////////////////////////////////////////////////////////////////// Instance
//////////////////////////////////////////////////////////////////////////////// Private
#region Field
/// <summary>
/// 디스플레이 정보
/// </summary>
private readonly DisplayInformation displayInformation = DisplayInformation.GetForCurrentView();
/// <summary>
/// 단순 방향 센서
/// </summary>
private readonly SimpleOrientationSensor orientationSensor = SimpleOrientationSensor.GetDefault();
/// <summary>
/// 장치 단순 방향
/// </summary>
private SimpleOrientation deviceOrientation = SimpleOrientation.NotRotated;
/// <summary>
/// 디스플레이 방향
/// </summary>
private DisplayOrientations displayOrientations = DisplayOrientations.Portrait;
/// <summary>
/// 캡처 저장소 폴더
/// </summary>
private StorageFolder captureStorageFolder = null;
/// <summary>
/// 디스플레이 요청
/// </summary>
private readonly DisplayRequest displayRequest = new DisplayRequest();
/// <summary>
/// 시스템 미디어 전송 컨트롤
/// </summary>
private readonly SystemMediaTransportControls systemMediaTransportControls = SystemMediaTransportControls.GetForCurrentView();
/// <summary>
/// 미디어 캡처
/// </summary>
private MediaCapture mediaCapture;
/// <summary>
/// 미디어 인코딩 속성
/// </summary>
private IMediaEncodingProperties mediaEncodingProperties;
/// <summary>
/// 초기화 여부
/// </summary>
private bool isInitialized;
/// <summary>
/// 레코딩 여부
/// </summary>
private bool isRecording;
/// <summary>
/// 미리보기 미러링 여부
/// </summary>
private bool mirroringPreview;
/// <summary>
/// 외부 카메라 여부
/// </summary>
private bool externalCamera;
/// <summary>
/// 얼굴 탐지 효과
/// </summary>
private FaceDetectionEffect faceDetectionEffect;
#endregion
//////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
////////////////////////////////////////////////////////////////////////////////////////// Public
#region 생성자 - MainPage()
/// <summary>
/// 생성자
/// </summary>
public MainPage()
{
InitializeComponent();
#region 윈도우 크기를 설정한다.
double width = 800d;
double height = 600d;
double dpi = (double)DisplayInformation.GetForCurrentView().LogicalDpi;
ApplicationView.PreferredLaunchWindowingMode = ApplicationViewWindowingMode.PreferredLaunchViewSize;
Size windowSize = new Size(width * 96d / dpi, height * 96d / dpi);
ApplicationView.PreferredLaunchViewSize = windowSize;
Window.Current.Activate();
ApplicationView.GetForCurrentView().TryResizeView(windowSize);
#endregion
#region 윈도우 제목을 설정한다.
ApplicationView.GetForCurrentView().Title = "카메라를 사용해 얼굴 탐지하기";
#endregion
NavigationCacheMode = NavigationCacheMode.Disabled;
Application.Current.Suspending += Application_Suspending;
Application.Current.Resuming += Application_Resuming;
}
#endregion
//////////////////////////////////////////////////////////////////////////////////////////////////// Method
////////////////////////////////////////////////////////////////////////////////////////// Protected
#region 탐색되는 경우 처리하기 - OnNavigatedTo(e)
/// <summary>
/// 탐색되는 경우 처리하기
/// </summary>
/// <param name="e">이벤트 인자</param>
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
await SetupUIAsync();
await InitializeCameraAsync();
}
#endregion
#region 탐색하는 경우 처리하기 - OnNavigatingFrom(e)
/// <summary>
/// 탐색하는 경우 처리하기
/// </summary>
/// <param name="e">이벤트 인자</param>
protected override async void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
await CleanupCameraAsync();
await CleanupUIAsync();
}
#endregion
////////////////////////////////////////////////////////////////////////////////////////// Private
//////////////////////////////////////////////////////////////////////////////// Event
#region 애플리케이션 일시 중단시 처리하기 - Application_Suspending(sender, e)
/// <summary>
/// 애플리케이션 일시 중단시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
private async void Application_Suspending(object sender, SuspendingEventArgs e)
{
if(Frame.CurrentSourcePageType == typeof(MainPage))
{
SuspendingDeferral deferral = e.SuspendingOperation.GetDeferral();
await CleanupCameraAsync();
await CleanupUIAsync();
deferral.Complete();
}
}
#endregion
#region 애플리케이션 재개시 처리하기 - Application_Resuming(sender, e)
/// <summary>
/// 애플리케이션 재개시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
private async void Application_Resuming(object sender, object e)
{
if(Frame.CurrentSourcePageType == typeof(MainPage))
{
await SetupUIAsync();
await InitializeCameraAsync();
}
}
#endregion
#region 단순 방향 센서 방향 변경시 처리하기 - orientationSensor_OrientationChanged(sender, e)
/// <summary>
/// 단순 방향 센서 방향 변경시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
private async void orientationSensor_OrientationChanged
(
SimpleOrientationSensor sender,
SimpleOrientationSensorOrientationChangedEventArgs e
)
{
if(e.Orientation != SimpleOrientation.Faceup && e.Orientation != SimpleOrientation.Facedown)
{
this.deviceOrientation = e.Orientation;
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => UpdateButtonOrientation());
}
}
#endregion
#region 디스플레이 정보 방향 변경시 처리하기 - displayInformation_OrientationChanged(sender, e)
/// <summary>
/// 디스플레이 정보 방향 변경시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
/// <remarks>
/// 이 이벤트는 SetupUIAsync 함수에 설정된 DisplayInformation.AutoRotationPreferences 값이 적용되지 않을 때
/// 페이지가 회전될 때 발생한다.
/// </remarks>
private async void displayInformation_OrientationChanged(DisplayInformation sender, object e)
{
this.displayOrientations = sender.CurrentOrientation;
if(this.mediaEncodingProperties != null)
{
await SetPreviewRotationAsync();
}
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => UpdateButtonOrientation());
}
#endregion
#region 미디어 캡처 레코드 제한 초과시 처리하기 - mediaCapture_RecordLimitationExceeded(sender)
/// <summary>
/// 미디어 캡처 레코드 제한 초과시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
private async void mediaCapture_RecordLimitationExceeded(MediaCapture sender)
{
// 녹음을 중지해야 하며 앱에서 녹음을 완료할 것으로 예상되는 알림이다.
await StopRecordingAsync();
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => UpdateCaptureControls());
}
#endregion
#region 미디어 캡처 실패시 처리하기 - mediaCapture_Failed(sender, e)
/// <summary>
/// 미디어 캡처 실패시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
private async void mediaCapture_Failed(MediaCapture sender, MediaCaptureFailedEventArgs e)
{
await CleanupCameraAsync();
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => UpdateCaptureControls());
}
#endregion
#region 시스템 미디어 전송 컨트롤 속성 변경시 처리하기 - systemMediaTransportControls_PropertyChanged(sender, e)
/// <summary>
/// 시스템 미디어 전송 컨트롤 속성 변경시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
/// <remarks>
/// 앱이 최소화되는 경우 이 메소드는 미디어 속성 변경 이벤트를 처리한다.
/// 앱이 음소거 알림을 수신하면 더 이상 포그라운드에 있지 않는다.
/// </remarks>
private async void systemMediaTransportControls_PropertyChanged
(
SystemMediaTransportControls sender,
SystemMediaTransportControlsPropertyChangedEventArgs e
)
{
await Dispatcher.RunAsync
(
CoreDispatcherPriority.Normal,
async () =>
{
// 이 페이지가 현재 표시되고 있는 경우에만 이 이벤트를 처리한다.
if
(
e.Property == SystemMediaTransportControlsProperty.SoundLevel &&
Frame.CurrentSourcePageType == typeof(MainPage)
)
{
// 앱이 음소거되고 있는지 확인한다. 그렇다면 최소화하고 있다.
// 그렇지 않으면 초기화되지 않은 경우 초점이 맞춰진다.
if(sender.SoundLevel == SoundLevel.Muted)
{
await CleanupCameraAsync();
}
else if (!this.isInitialized)
{
await InitializeCameraAsync();
}
}
}
);
}
#endregion
#region 얼굴 탐지 효과 얼굴 탐지시 처리하기 - faceDetectionEffect_FaceDetected(sender, e)
/// <summary>
/// 얼굴 탐지 효과 얼굴 탐지시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
private async void faceDetectionEffect_FaceDetected(FaceDetectionEffect sender, FaceDetectedEventArgs e)
{
await Dispatcher.RunAsync
(
CoreDispatcherPriority.Normal,
() => HighlightDetectedFaces(e.ResultFrame.DetectedFaces)
);
}
#endregion
#region 하드웨어 버튼 카메라 PRESS 처리하기 - HardwareButtons_CameraPressed(sender, e)
/// <summary>
/// 하드웨어 버튼 카메라 PRESS 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
private async void HardwareButtons_CameraPressed(object sender, CameraEventArgs e)
{
await TakePhotoAsync();
}
#endregion
#region 사진 버튼 클릭시 처리하기 - photoButton_Click(sender, e)
/// <summary>
/// 사진 버튼 클릭시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
private async void photoButton_Click(object sender, RoutedEventArgs e)
{
await TakePhotoAsync();
}
#endregion
#region 비디오 버튼 클릭시 처리하기 - videoButton_Click(sender, e)
/// <summary>
/// 비디오 버튼 클릭시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
private async void videoButton_Click(object sender, RoutedEventArgs e)
{
if(!this.isRecording)
{
await StartRecordingAsync();
}
else
{
await StopRecordingAsync();
}
UpdateCaptureControls();
}
#endregion
#region 얼굴 탐지 버튼 클릭시 처리하기 - faceDetectionButton_Click(sender, e)
/// <summary>
/// 얼굴 탐지 버튼 클릭시 처리하기
/// </summary>
/// <param name="sender">이벤트 발생자</param>
/// <param name="e">이벤트 인자</param>
private async void faceDetectionButton_Click(object sender, RoutedEventArgs e)
{
if(this.faceDetectionEffect == null || !this.faceDetectionEffect.Enabled)
{
this.faceCanvas.Children.Clear();
await CreateFaceDetectionEffectAsync();
}
else
{
await CleanUpFaceDetectionEffectAsync();
}
UpdateCaptureControls();
}
#endregion
//////////////////////////////////////////////////////////////////////////////// Function
#region 카메라 방향 구하기 - GetCameraOrientation()
/// <summary>
/// 카메라 방향 구하기
/// </summary>
/// <returns>카메라 방향</returns>
/// <remarks>
/// 카메라가 외부에 있는지 또는 사용자를 향하고 있는지를 고려하여 장치 방향에서 현재 카메라 방향을 계산한다.
/// </remarks>
private SimpleOrientation GetCameraOrientation()
{
if(this.externalCamera)
{
return SimpleOrientation.NotRotated;
}
SimpleOrientation targetOrientation = this.deviceOrientation;
// 세로 우선 장치에서 카메라 센서가 기본 방향에 대해 90도 오프셋으로 장착된다는 사실을 고려한다.
if(this.displayInformation.NativeOrientation == DisplayOrientations.Portrait)
{
switch(targetOrientation)
{
case SimpleOrientation.Rotated90DegreesCounterclockwise :
targetOrientation = SimpleOrientation.NotRotated;
break;
case SimpleOrientation.Rotated180DegreesCounterclockwise :
targetOrientation = SimpleOrientation.Rotated90DegreesCounterclockwise;
break;
case SimpleOrientation.Rotated270DegreesCounterclockwise :
targetOrientation = SimpleOrientation.Rotated180DegreesCounterclockwise;
break;
case SimpleOrientation.NotRotated :
targetOrientation = SimpleOrientation.Rotated270DegreesCounterclockwise;
break;
}
}
// 전면 카메라에 대해 미리보기가 미러링되는 경우 회전이 반전되어야 한다.
if(this.mirroringPreview)
{
// 이것은 90도와 270도 경우에만 영향을 미친다.
// 0도와 180도 회전은 시계 방향과 반시계 방향이 동일하기 때문이다.
switch(targetOrientation)
{
case SimpleOrientation.Rotated90DegreesCounterclockwise : return SimpleOrientation.Rotated270DegreesCounterclockwise;
case SimpleOrientation.Rotated270DegreesCounterclockwise : return SimpleOrientation.Rotated90DegreesCounterclockwise;
}
}
return targetOrientation;
}
#endregion
#region 사진 방향 구하기 - GetPhotoOrientation(cameraOrientation)
/// <summary>
/// 사진 방향 구하기
/// </summary>
/// <param name="cameraOrientation">카메라 방향</param>
/// <returns>사진 방향</returns>
private static PhotoOrientation GetPhotoOrientation(SimpleOrientation cameraOrientation)
{
switch(cameraOrientation)
{
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;
}
}
#endregion
#region 장치 방향 각도 구하기 - GetDeviceOrientationDegrees(deviceOrientation)
/// <summary>
/// 장치 방향 각도 구하기
/// </summary>
/// <param name="deviceOrientation">장치 방향</param>
/// <returns>장치 방향 각도</returns>
private static int GetDeviceOrientationDegrees(SimpleOrientation deviceOrientation)
{
switch(deviceOrientation)
{
case SimpleOrientation.Rotated90DegreesCounterclockwise : return 90;
case SimpleOrientation.Rotated180DegreesCounterclockwise : return 180;
case SimpleOrientation.Rotated270DegreesCounterclockwise : return 270;
case SimpleOrientation.NotRotated :
default : return 0;
}
}
#endregion
#region 디스플레이 방향 각도 구하기 - GetDisplayOrientationDegrees(displayOrientations)
/// <summary>
/// 디스플레이 방향 각도 구하기
/// </summary>
/// <param name="displayOrientations">디스플레이 방향</param>
/// <returns>디스플레이 방향 각도 구하기</returns>
private static int GetDisplayOrientationDegrees(DisplayOrientations displayOrientations)
{
switch(displayOrientations)
{
case DisplayOrientations.Portrait : return 90;
case DisplayOrientations.LandscapeFlipped : return 180;
case DisplayOrientations.PortraitFlipped : return 270;
case DisplayOrientations.Landscape :
default : return 0;
}
}
#endregion
#region 카메라 장치 정보 구하기 - GetCameraDeviceInformation(targetPanel)
/// <summary>
/// 카메라 장치 정보 구하기
/// </summary>
/// <param name="targetPanel">타겟 패널</param>
/// <returns>카메라 장치 정보</returns>
private static async Task<DeviceInformation> GetCameraDeviceInformation
(
Windows.Devices.Enumeration.Panel targetPanel
)
{
// 사진 캡처에 사용할 수 있는 장치를 가져온다.
DeviceInformationCollection collection = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture);
// 패널별로 원하는 카메라를 가져온다.
DeviceInformation deviceInformation = collection.FirstOrDefault
(
x => x.EnclosureLocation != null &&
x.EnclosureLocation.Panel == targetPanel
);
// 원하는 패널에 장착된 장치가 없으면 처음 발견된 장치를 반환한다.
return deviceInformation ?? collection.FirstOrDefault();
}
#endregion
#region 버튼 방향 업데이트하기 - UpdateButtonOrientation()
/// <summary>
/// 버튼 방향 업데이트하기
/// </summary>
/// <remarks>
/// 공간의 현재 장치 방향과 화면의 페이지 방향을 사용하여 컨트롤에 적용할 회전 변환을 계산합니다.
/// </remarks>
private void UpdateButtonOrientation()
{
int deviceOrientationDegrees = GetDeviceOrientationDegrees (this.deviceOrientation );
int displayOritentationDegrees = GetDisplayOrientationDegrees(this.displayOrientations);
if(this.displayInformation.NativeOrientation == DisplayOrientations.Portrait)
{
deviceOrientationDegrees -= 90;
}
int angle = (360 + displayOritentationDegrees + deviceOrientationDegrees) % 360;
RotateTransform transform = new RotateTransform { Angle = angle };
this.photoButton.RenderTransform = transform;
this.videoButton.RenderTransform = transform;
this.faceDetectionButton.RenderTransform = transform;
}
#endregion
#region 캡처 컨트롤 업데이트히기 - UpdateCaptureControls()
/// <summary>
/// 캡처 컨트롤 업데이트히기
/// </summary>
/// <remarks>
/// 이 방법은 앱의 현재 상태와 장치의 기능에 따라 아이콘을 업데이트하고
/// 사진/비디오 버튼을 활성화/비활성화하고 표시하거나/숨긴다.
/// </remarks>
private void UpdateCaptureControls()
{
// 버튼은 미리보기가 성공적으로 시작된 경우에만 활성화되어야 한다.
this.photoButton.IsEnabled = this.mediaEncodingProperties != null;
this.videoButton.IsEnabled = this.mediaEncodingProperties != null;
this.faceDetectionButton.IsEnabled = this.mediaEncodingProperties != null;
// 효과 유무에 따라 얼굴 탐지 아이콘을 업데이트한다.
this.faceDetectionSymbolIcon1.Visibility =
(this.faceDetectionEffect != null && this.faceDetectionEffect.Enabled) ? Visibility.Visible : Visibility.Collapsed;
this.faceDetectionSymbolIcon2.Visibility =
(this.faceDetectionEffect == null || !this.faceDetectionEffect.Enabled) ? Visibility.Visible : Visibility.Collapsed;
// 얼굴 탐지 캔버스를 숨기고 지운다.
this.faceCanvas.Visibility =
(this.faceDetectionEffect != null && this.faceDetectionEffect.Enabled) ? Visibility.Visible : Visibility.Collapsed;
// 레코딩시 적색 "레코딩" 아이콘 대신 "중지" 아이콘을 표시하도록 레코딩 버튼을 업데이트한다.
this.startVideoEllipse.Visibility = this.isRecording ? Visibility.Collapsed : Visibility.Visible;
this.stopVideoRectangle.Visibility = this.isRecording ? Visibility.Visible : Visibility.Collapsed;
// 카메라가 사진 촬영과 비디오 녹화를 동시에 지원하지 않는 경우 녹화에서 사진 버튼을 비활성화한다.
if(this.isInitialized && !this.mediaCapture.MediaCaptureSettings.ConcurrentRecordAndPhotoSupported)
{
this.photoButton.IsEnabled = !this.isRecording;
// 비활성화된 경우 버튼을 보이지 않게 하여 상호 작용할 수 없음을 분명히 한다.
this.photoButton.Opacity = this.photoButton.IsEnabled ? 1 : 0;
}
}
#endregion
#region 이벤트 핸들러 등록하기 - RegisterEventHandlers()
/// <summary>
/// 이벤트 핸들러 등록하기
/// </summary>
private void RegisterEventHandlers()
{
if(ApiInformation.IsTypePresent("Windows.Phone.UI.Input.HardwareButtons"))
{
HardwareButtons.CameraPressed += HardwareButtons_CameraPressed;
}
if(this.orientationSensor != null)
{
this.orientationSensor.OrientationChanged += orientationSensor_OrientationChanged;
UpdateButtonOrientation();
}
this.displayInformation.OrientationChanged += displayInformation_OrientationChanged;
this.systemMediaTransportControls.PropertyChanged += systemMediaTransportControls_PropertyChanged;
}
#endregion
#region 이벤트 핸들러 등록취소하기 - UnregisterEventHandlers()
/// <summary>
/// 이벤트 핸들러 등록취소하기
/// </summary>
private void UnregisterEventHandlers()
{
if(ApiInformation.IsTypePresent("Windows.Phone.UI.Input.HardwareButtons"))
{
HardwareButtons.CameraPressed -= HardwareButtons_CameraPressed;
}
if(this.orientationSensor != null)
{
this.orientationSensor.OrientationChanged -= orientationSensor_OrientationChanged;
}
this.displayInformation.OrientationChanged -= displayInformation_OrientationChanged;
this.systemMediaTransportControls.PropertyChanged -= systemMediaTransportControls_PropertyChanged;
}
#endregion
#region UI 셋업하기 (비동기) - SetupUIAsync()
/// <summary>
/// UI 셋업하기 (비동기)
/// </summary>
/// <returns>태스크</returns>
/// <remarks>
/// 페이지 방향을 잠그고 상태 표시줄(전화에서)을 숨기고
/// 하드웨어 버튼 및 방향 센서에 대해 등록된 이벤트 핸들러를 숨기려고 시도합니다.
/// </remarks>
private async Task SetupUIAsync()
{
// 더 나은 경험을 제공하므로 CaptureElement가 회전하지 않도록 페이지를 가로 방향으로 잠그도록 시도한다.
DisplayInformation.AutoRotationPreferences = DisplayOrientations.Landscape;
// 상태 표시줄 숨긴다.
if(ApiInformation.IsTypePresent("Windows.UI.ViewManagement.StatusBar"))
{
await Windows.UI.ViewManagement.StatusBar.GetForCurrentView().HideAsync();
}
// 현재 상태로 방향 변수를 채운다.
this.displayOrientations = this.displayInformation.CurrentOrientation;
if(this.orientationSensor != null)
{
this.deviceOrientation = this.orientationSensor.GetCurrentOrientation();
}
RegisterEventHandlers();
StorageLibrary picturesStorageLibrary = await StorageLibrary.GetLibraryAsync(KnownLibraryId.Pictures);
this.captureStorageFolder = picturesStorageLibrary.SaveFolder ?? ApplicationData.Current.LocalFolder;
}
#endregion
#region UI 청소하기 (비동기) - CleanupUIAsync()
/// <summary>
/// UI 청소하기 (비동기)
/// </summary>
/// <returns></returns>
/// <remarks>
/// 하드웨어 버튼 및 방향 센서에 대한 이벤트 핸들러를 등록취소하고,
/// 상태 표시줄(전화기에 대한) 표시를 허용하고,
/// 페이지 방향 잠금을 제거한다.
/// </remarks>
private async Task CleanupUIAsync()
{
UnregisterEventHandlers();
if(ApiInformation.IsTypePresent("Windows.UI.ViewManagement.StatusBar"))
{
await Windows.UI.ViewManagement.StatusBar.GetForCurrentView().ShowAsync();
}
DisplayInformation.AutoRotationPreferences = DisplayOrientations.None;
}
#endregion
#region 카메라 초기화하기 (비동기) - InitializeCameraAsync()
/// <summary>
/// 카메라 초기화하기 (비동기)
/// </summary>
/// <returns></returns>
private async Task InitializeCameraAsync()
{
if(this.mediaCapture == null)
{
// 가능한 경우 전면 카메라를 가져오는 것을 시도하고, 그렇지 않은 경우 다른 카메라 장치를 사용한다.
DeviceInformation cameraDeviceInformation = await GetCameraDeviceInformation
(
Windows.Devices.Enumeration.Panel.Front
);
if(cameraDeviceInformation == null)
{
return;
}
this.mediaCapture = new MediaCapture();
// 동영상 녹화가 최대 시간에 도달했을 때와 문제가 발생했을 때 이벤트를 등록한다.
this.mediaCapture.RecordLimitationExceeded += mediaCapture_RecordLimitationExceeded;
this.mediaCapture.Failed += mediaCapture_Failed;
MediaCaptureInitializationSettings settings = new MediaCaptureInitializationSettings
{
VideoDeviceId = cameraDeviceInformation.Id
};
try
{
await this.mediaCapture.InitializeAsync(settings);
this.isInitialized = true;
}
catch(UnauthorizedAccessException)
{
}
if(this.isInitialized)
{
if
(
cameraDeviceInformation.EnclosureLocation == null ||
cameraDeviceInformation.EnclosureLocation.Panel == Windows.Devices.Enumeration.Panel.Unknown
)
{
this.externalCamera = true;
}
else
{
this.externalCamera = false;
this.mirroringPreview =
(cameraDeviceInformation.EnclosureLocation.Panel == Windows.Devices.Enumeration.Panel.Front);
}
await StartPreviewAsync();
UpdateCaptureControls();
}
}
}
#endregion
#region 카메라 청소하기 (비동기) - CleanupCameraAsync()
/// <summary>
/// 카메라 청소하기 (비동기)
/// </summary>
/// <returns>태스크</returns>
/// <remarks>
/// 카메라 리소스를 정리하고(비디오 녹화 및/또는 필요한 경우 미리 보기를 중지한 후)
/// MediaCapture 이벤트에서 등록을 취소한다.
/// </remarks>
private async Task CleanupCameraAsync()
{
if(this.isInitialized)
{
// 정리 중 녹음이 진행 중인 경우 중지하여 녹음을 저장한다.
if(this.isRecording)
{
await StopRecordingAsync();
}
if(this.faceDetectionEffect != null)
{
await CleanUpFaceDetectionEffectAsync();
}
if(this.mediaEncodingProperties != null)
{
// 미리 보기를 중지하는 호출은 완성을 위해 여기에 포함되지만
// 나중에 MediaCapture.Dispose()에 대한 호출이 수행되는 경우
// 미리 보기가 해당 지점에서 자동으로 중지되므로 안전하게 제거할 수 있다.
await StopPreviewAsync();
}
this.isInitialized = false;
}
if(this.mediaCapture != null)
{
this.mediaCapture.RecordLimitationExceeded -= mediaCapture_RecordLimitationExceeded;
this.mediaCapture.Failed -= mediaCapture_Failed;
this.mediaCapture.Dispose();
this.mediaCapture = null;
}
}
#endregion
#region 미리보기 사각형 구하기 - GetPreviewRectangle(previewResolution, previewControl)
/// <summary>
/// 미리보기 사각형 구하기
/// </summary>
/// <param name="previewResolution">미리보기 비디오 인코딩 속성</param>
/// <param name="previewControl">미리보기 컨트롤</param>
/// <returns>미리보기 사각형</returns>
/// <remarks>
/// 크기 조정 모드가 균일일 때 미리 보기 컨트롤 내에서 미리 보기 스트림을 포함하는 사각형의 크기와 위치를 계산한다.
/// </remarks>
public Rect GetPreviewRectangle(VideoEncodingProperties previewResolution, CaptureElement previewControl)
{
Rect previewRectangle = new Rect();
// 모든 것이 올바르게 초기화되기 전에 이 함수가 호출된 경우 빈 결과를 반환한다.
if
(
previewControl == null ||
previewControl.ActualHeight < 1 ||
previewControl.ActualWidth < 1 ||
previewResolution == null ||
previewResolution.Height == 0 ||
previewResolution.Width == 0
)
{
return previewRectangle;
}
uint streamWidth = previewResolution.Width;
uint streamHeight = previewResolution.Height;
// 세로 방향의 경우 너비와 높이를 바꿔야 한다.
if
(
this.displayOrientations == DisplayOrientations.Portrait ||
this.displayOrientations == DisplayOrientations.PortraitFlipped
)
{
streamWidth = previewResolution.Height;
streamHeight = previewResolution.Width;
}
// 컨트롤의 미리보기 표시 영역이 전체 너비와 높이에 걸쳐 있다고
// 가정하여 시작한다(필요한 치수의 경우 다음에서 수정된다).
previewRectangle.Width = previewControl.ActualWidth;
previewRectangle.Height = previewControl.ActualHeight;
// UI가 미리보기보다 "넓은" 경우 레터박스가 측면에 표시된다.
if((previewControl.ActualWidth / previewControl.ActualHeight > streamWidth / (double)streamHeight))
{
double scale = previewControl.ActualHeight / streamHeight;
double scaledWidth = streamWidth * scale;
previewRectangle.X = (previewControl.ActualWidth - scaledWidth) / 2.0;
previewRectangle.Width = scaledWidth;
}
else // 미리보기 스트림은 UI보다 "넓음"이므로 레터박스가 상단+하단에 표시된다.
{
double scale = previewControl.ActualWidth / streamWidth;
double scaledHeight = streamHeight * scale;
previewRectangle.Y = (previewControl.ActualHeight - scaledHeight) / 2.0;
previewRectangle.Height = scaledHeight;
}
return previewRectangle;
}
#endregion
#region 미리보기 회전 설정하기 (비동기) - SetPreviewRotationAsync()
/// <summary>
/// 미리보기 회전 설정하기 (비동기)
/// </summary>
/// <returns>태스크</returns>
/// <remarks>
/// 장치와 관련된 UI의 현재 방향을 가져오고(AutoRotationPreferences를 준수할 수 없는 경우)
/// 미리 보기에 수정 회전을 적용한다.
/// </remarks>
private async Task SetPreviewRotationAsync()
{
// 카메라가 장치에 장착된 경우에만 방향을 업데이트해야 한다.
if(this.externalCamera)
{
return;
}
// 미리보기를 회전할 방향과 거리를 계산한다.
int rotationDegrees = GetDisplayOrientationDegrees(this.displayOrientations);
// 미리보기가 미러링되는 경우 회전 방향을 반전해야 한다.
if(this.mirroringPreview)
{
rotationDegrees = (360 - rotationDegrees) % 360;
}
// 미리보기 스트림에 회전 메타 데이터를 추가하여 미리보기 프레임을 렌더링하고 가져올 때
// 종횡비/치수가 일치하도록 한다.
IMediaEncodingProperties properties = this.mediaCapture.VideoDeviceController.GetMediaStreamProperties
(
MediaStreamType.VideoPreview
);
properties.Properties.Add(_rotationKeyGUID, rotationDegrees);
await this.mediaCapture.SetEncodingPropertiesAsync(MediaStreamType.VideoPreview, properties, null);
}
#endregion
#region 미리보기 시작하기 (비동기) - StartPreviewAsync()
/// <summary>
/// 미리보기 시작하기 (비동기)
/// </summary>
/// <returns>태스크</returns>
/// <remarks>
/// 미리보기를 시작하고 화면을 켜두도록 요청한 후 회전 및 미러링을 위해 조정한다.
/// </remarks>
private async Task StartPreviewAsync()
{
// 미리보기가 실행되는 동안 장치가 절전 모드로 전환되지 않도록 방지한다.
this.displayRequest.RequestActive();
// UI에서 미리보기 소스를 설정하고 필요한 경우 미러링을 처리한다.
this.previewCaptureElement.Source = this.mediaCapture;
this.previewCaptureElement.FlowDirection = this.mirroringPreview ? FlowDirection.RightToLeft : FlowDirection.LeftToRight;
// 미리보기를 시작한다.
await this.mediaCapture.StartPreviewAsync();
this.mediaEncodingProperties = this.mediaCapture.VideoDeviceController.GetMediaStreamProperties
(
MediaStreamType.VideoPreview
);
// 현재 방향으로 미리보기를 초기화한다.
if(this.mediaEncodingProperties != null)
{
this.displayOrientations = this.displayInformation.CurrentOrientation;
await SetPreviewRotationAsync();
}
}
#endregion
#region 미리보기 중단하기 (비동기) - StopPreviewAsync()
/// <summary>
/// 미리보기 중단하기 (비동기)
/// </summary>
/// <returns>태스크</returns>
/// <remarks>
/// 미리보기를 중지하고 화면을 절전 모드로 전환할 수 있도록 표시 요청을 비활성화한다.
/// </remarks>
private async Task StopPreviewAsync()
{
// 미리보기를 중단한다.
this.mediaEncodingProperties = null;
await this.mediaCapture.StopPreviewAsync();
// 이 메소드는 UI가 아닌 스레드에서 호출되는 경우가 있으므로 디스패처를 사용합니다.
await Dispatcher.RunAsync
(
CoreDispatcherPriority.Normal,
() =>
{
// UI를 청소한다.
this.previewCaptureElement.Source = null;
// 미리보기가 중지되었으므로 이제 장치 화면을 절전 모드로 전환한다.
this.displayRequest.RequestRelease();
}
);
}
#endregion
#region 레코딩 시작하기 (비동기) - StartRecordingAsync()
/// <summary>
/// 레코딩 시작하기 (비동기)
/// </summary>
/// <returns></returns>
/// <remarks>
/// MP4 비디오를 StorageFile에 녹화하고 회전 메타데이터를 추가한다.
/// </remarks>
private async Task StartRecordingAsync()
{
try
{
StorageFile videoStorageFile = await this.captureStorageFolder.CreateFileAsync
(
"SimpleVideo.mp4",
CreationCollisionOption.GenerateUniqueName
);
MediaEncodingProfile mediaEncodingProfile = MediaEncodingProfile.CreateMp4(VideoEncodingQuality.Auto);
// 필요한 경우 미러링을 고려하여 회전 각도를 계산한다.
int rotationAngle = 360 - GetDeviceOrientationDegrees(GetCameraOrientation());
mediaEncodingProfile.Video.Properties.Add(_rotationKeyGUID, PropertyValue.CreateInt32(rotationAngle));
await this.mediaCapture.StartRecordToStorageFileAsync(mediaEncodingProfile, videoStorageFile);
this.isRecording = true;
}
catch
{
}
}
#endregion
#region 레코딩 중단하기 (비동기) - StopRecordingAsync()
/// <summary>
/// 레코딩 중단하기 (비동기)
/// </summary>
/// <returns>태스크</returns>
private async Task StopRecordingAsync()
{
this.isRecording = false;
await this.mediaCapture.StopRecordAsync();
}
#endregion
#region 사진 저장하기 (비동기) - SavePhotoAsync(stream, targetFile, photoOrientation)
/// <summary>
/// 사진 저장하기 (비동기)
/// </summary>
/// <param name="stream">랜덤 액세스 스트림</param>
/// <param name="targetFile">타겟 저장소 파일</param>
/// <param name="photoOrientation">사진 방향</param>
/// <returns>태스크</returns>
private static async Task SavePhotoAsync
(
IRandomAccessStream stream,
StorageFile targetFile,
PhotoOrientation photoOrientation
)
{
using(IRandomAccessStream sourceStream = stream)
{
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(sourceStream);
using(IRandomAccessStream targetStream = await targetFile.OpenAsync(FileAccessMode.ReadWrite))
{
BitmapEncoder encoder = await BitmapEncoder.CreateForTranscodingAsync(targetStream, decoder);
BitmapPropertySet bitmapPropertySet = new BitmapPropertySet
{
{
"System.Photo.Orientation",
new BitmapTypedValue(photoOrientation, PropertyType.UInt16)
}
};
await encoder.BitmapProperties.SetPropertiesAsync(bitmapPropertySet);
await encoder.FlushAsync();
}
}
}
#endregion
#region 사진 촬영하기 (비동기) - TakePhotoAsync()
/// <summary>
/// 사진 촬영하기 (비동기)
/// </summary>
/// <returns>태스크</returns>
private async Task TakePhotoAsync()
{
this.videoButton.IsEnabled = this.mediaCapture.MediaCaptureSettings.ConcurrentRecordAndPhotoSupported;
this.videoButton.Opacity = this.videoButton.IsEnabled ? 1 : 0;
InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream();
await this.mediaCapture.CapturePhotoToStreamAsync(ImageEncodingProperties.CreateJpeg(), stream);
try
{
StorageFile storageFile = await this.captureStorageFolder.CreateFileAsync
(
"SimplePhoto.jpg",
CreationCollisionOption.GenerateUniqueName
);
PhotoOrientation photoOrientation = GetPhotoOrientation(GetCameraOrientation());
await SavePhotoAsync(stream, storageFile, photoOrientation);
}
catch
{
}
this.videoButton.IsEnabled = true;
this.videoButton.Opacity = 1;
}
#endregion
#region 얼굴 탐지 효과 생성하기 (비동기) - CreateFaceDetectionEffectAsync()
/// <summary>
/// 얼굴 탐지 효과 생성하기 (비동기)
/// </summary>
/// <returns>태스크</returns>
private async Task CreateFaceDetectionEffectAsync()
{
FaceDetectionEffectDefinition definition = new FaceDetectionEffectDefinition();
definition.SynchronousDetectionEnabled = false;
definition.DetectionMode = FaceDetectionMode.HighPerformance;
this.faceDetectionEffect = await this.mediaCapture.AddVideoEffectAsync
(
definition,
MediaStreamType.VideoPreview
) as FaceDetectionEffect;
this.faceDetectionEffect.FaceDetected += faceDetectionEffect_FaceDetected;
this.faceDetectionEffect.DesiredDetectionInterval = TimeSpan.FromMilliseconds(33);
this.faceDetectionEffect.Enabled = true;
}
#endregion
#region 얼굴 탐지 효과 청소하기 (비동기) - CleanUpFaceDetectionEffectAsync()
/// <summary>
/// 얼굴 탐지 효과 청소하기 (비동기)
/// </summary>
/// <returns>태스크</returns>
/// <remarks>
/// 얼굴 탐지 효과를 비활성화 및 제거하고 얼굴 탐지를 위한 이벤트 처리기를 등록 취소한다.
/// </remarks>
private async Task CleanUpFaceDetectionEffectAsync()
{
this.faceDetectionEffect.Enabled = false;
this.faceDetectionEffect.FaceDetected -= faceDetectionEffect_FaceDetected;
await this.mediaCapture.RemoveEffectAsync(this.faceDetectionEffect);
this.faceDetectionEffect = null;
}
#endregion
#region 얼굴 캔버스 회전 설정하기 - SetFaceCanvasRotation()
/// <summary>
/// 얼굴 캔버스 회전 설정하기
/// </summary>
/// <remarks>
/// 현재 표시 방향을 사용하여 얼굴 탐지 캔버스에 적용할 회전 변환을 계산하고
/// 미리보기가 미러링되는 경우 미러링한다.
/// </remarks>
private void SetFaceCanvasRotation()
{
// 캔버스를 얼마나 회전시킬지 계산한다.
int rotationDegrees = GetDisplayOrientationDegrees(this.displayOrientations);
// SetPreviewRotationAsync와 마찬가지로 미리보기가 미러링되는 경우 회전 방향을 반전해야 한다.
if(this.mirroringPreview)
{
rotationDegrees = (360 - rotationDegrees) % 360;
}
// 회전을 적용한다.
RotateTransform transform = new RotateTransform { Angle = rotationDegrees };
this.faceCanvas.RenderTransform = transform;
Rect previewRect = GetPreviewRectangle
(
this.mediaEncodingProperties as VideoEncodingProperties,
this.previewCaptureElement
);
// 세로 모드 방향의 경우 회전 후 캔버스의 너비와 높이를 바꿔 컨트롤이 계속 미리보기와 겹치도록 한다.
if
(
this.displayOrientations == DisplayOrientations.Portrait ||
this.displayOrientations == DisplayOrientations.PortraitFlipped
)
{
this.faceCanvas.Width = previewRect.Height;
this.faceCanvas.Height = previewRect.Width;
// 크기 조정이 컨트롤의 중앙에 영향을 미치므로 캔버스의 위치도 조정해야 한다.
Canvas.SetLeft(this.faceCanvas, previewRect.X - (previewRect.Height - previewRect.Width ) / 2);
Canvas.SetTop (this.faceCanvas, previewRect.Y - (previewRect.Width - previewRect.Height) / 2);
}
else
{
this.faceCanvas.Width = previewRect.Width;
this.faceCanvas.Height = previewRect.Height;
Canvas.SetLeft(this.faceCanvas, previewRect.X);
Canvas.SetTop (this.faceCanvas, previewRect.Y);
}
// 미리보기가 미러링되는 경우 캔버스도 미러링한다.
this.faceCanvas.FlowDirection = this.mirroringPreview ? FlowDirection.RightToLeft : FlowDirection.LeftToRight;
}
#endregion
#region 얼굴 사각형 구하기 - GetFaceRectangle(faceBitmapBounds)
/// <summary>
/// 얼굴 사각형 구하기
/// </summary>
/// <param name="faceBitmapBounds">얼굴 비트맵 테두리</param>
/// <returns>얼굴 사각형</returns>
/// <remarks>
/// 미리보기 좌표에 정의된 얼굴 정보를 가져와 미리보기 컨트롤의 위치와 크기를 고려하여 UI 좌표로 반환한다.
/// </remarks>
private Rectangle GetFaceRectangle(BitmapBounds faceBitmapBounds)
{
Rectangle rectangle = new Rectangle();
VideoEncodingProperties previewProperties = this.mediaEncodingProperties as VideoEncodingProperties;
// 미리보기에 대한 사용 가능한 정보가 없으면 화면 좌표로 다시 크기 조정이 불가능하므로 빈 사각형을 반환한다.
if(previewProperties == null)
{
return rectangle;
}
// 유사하게, 차원 중 하나라도 0이면(오류 경우에만 발생) 빈 사각형을 반환한다.
if(previewProperties.Width == 0 || previewProperties.Height == 0)
{
return rectangle;
}
double streamWidth = previewProperties.Width;
double streamHeight = previewProperties.Height;
// 세로 방향의 경우 너비와 높이를 바꿔야 한다.
if
(
this.displayOrientations == DisplayOrientations.Portrait ||
this.displayOrientations == DisplayOrientations.PortraitFlipped
)
{
streamHeight = previewProperties.Width;
streamWidth = previewProperties.Height;
}
// 실제 비디오 피드가 차지하는 사각형을 가져온다.
Rect previewInUI = GetPreviewRectangle(previewProperties, previewCaptureElement);
// 미리보기 스트림 좌표에서 창 좌표로 너비 및 높이를 크기 조정한다.
rectangle.Width = (faceBitmapBounds.Width / streamWidth ) * previewInUI.Width;
rectangle.Height = (faceBitmapBounds.Height / streamHeight) * previewInUI.Height;
// 미리보기 스트림 좌표에서 창 좌표로 X 및 Y 좌표를 크기 조정한다.
double x = (faceBitmapBounds.X / streamWidth ) * previewInUI.Width;
double y = (faceBitmapBounds.Y / streamHeight) * previewInUI.Height;
Canvas.SetLeft(rectangle, x);
Canvas.SetTop (rectangle, y);
return rectangle;
}
#endregion
#region 탐지 얼굴 하이라이트 처리하기 - HighlightDetectedFaces(detectedFaceList)
/// <summary>
/// 탐지 얼굴 하이라이트 처리하기
/// </summary>
/// <param name="detectedFaceList">탐지 얼굴 리스트</param>
/// <remarks>
/// 탐지된 모든 얼굴에 대해 반복하여 Rectangle을 생성하고 FaceCanvas에 얼굴 탐지 상자로 추가한다.
/// </remarks>
private void HighlightDetectedFaces(IReadOnlyList<DetectedFace> detectedFaceList)
{
// 이전 이벤트에서 기존 사각형을 제거한다.
this.faceCanvas.Children.Clear();
for(int i = 0; i < detectedFaceList.Count; i++)
{
// 얼굴 좌표 단위는 미리보기 해상도 픽셀로 디스플레이 해상도와 스케일이 다를 수 있으므로 변환이 필요할 수 있다.
Rectangle faceRectangle = GetFaceRectangle(detectedFaceList[i].FaceBox);
faceRectangle.StrokeThickness = 2;
faceRectangle.Stroke = (i == 0 ? new SolidColorBrush(Colors.Blue) : new SolidColorBrush(Colors.DeepSkyBlue));
this.faceCanvas.Children.Add(faceRectangle);
}
// 얼굴 캔버스 회전을 설정한다.
SetFaceCanvasRotation();
}
#endregion
}
}