Eventos

[.NET] ¿Cómo grabar vídeos en Windows Phone Silverlight?

Hace poco tuve que aprender a progamar para subir vídeos en Windows Phone. Al parecer habían un par de formas de hacerlo, así que a continuación compartiré la que me funcionó y la que creo que fue la mejor alternativa. Se aceptan sugerencias :)!

Para mi caso en especial todo gira alrededor de la siguiente clase:

namespace blog.epicalsoft.com.Model
{
public class Evidence
{
public int Id { get; set; }
public EvidenceKind Kind { get; set; }
public string ResSrc { get; set; }
public string ResExt { get; set; }
public string TnSrc { get; set; }
public string TnExt { get; set; }
public object Data { get; set; }
}
public enum EvidenceKind
{
Image = 1,
Audio = 2,
Video = 3,
Other = 99
}
}
view raw Evidence.cs hosted with ❤ by GitHub

En el proyecto Windows Phone, empezamos actualizando el WMAppManifest.xml para poder usar la cámara y el micrófono.

<Capabilities>
<Capability Name="ID_CAP_NETWORKING" />
<Capability Name="ID_CAP_LOCATION" />
<Capability Name="ID_CAP_PUSH_NOTIFICATION" />
<Capability Name="ID_CAP_ISV_CAMERA" />
<Capability Name="ID_CAP_MEDIALIB_PHOTO" />
<Capability Name="ID_CAP_MAP" />
<Capability Name="ID_CAP_WEBBROWSERCOMPONENT" />
<Capability Name="ID_CAP_CONTACTS" />
<Capability Name="ID_CAP_PHONEDIALER" />
<Capability Name="ID_CAP_MICROPHONE" />
</Capabilities>

Luego de ello ya podemos definir nuestra pantalla como mejor creamos.

<phone:PhoneApplicationPage
x:Class="blog.epicalsoft.com.Controls.VideoCapturePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
SupportedOrientations="Landscape"
mc:Ignorable="d"
shell:SystemTray.IsVisible="False" Orientation="LandscapeLeft" >
<Grid x:Name="LayoutRoot">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Canvas x:Name="Canvas" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Width="640"
Height="480" RenderTransformOrigin="0.5,0.5">
<Canvas.RenderTransform>
<RotateTransform x:Name="CanvasRotateTransform" ></RotateTransform>
</Canvas.RenderTransform>
</Canvas>
<Grid>
<TextBlock x:Name="Sizes" Text="" FontSize="32" FontFamily="Segoe UI Light"/>
<Image x:Name="ThumbnailImage" />
<StackPanel Orientation="Horizontal" Margin="12" VerticalAlignment="Bottom" HorizontalAlignment="Left">
<Button Visibility="Collapsed" Margin="12" x:Name="PLAY" Style="{StaticResource ButtonEmptyStyle}" HorizontalAlignment="Left" VerticalAlignment="Bottom" Tap="PLAY_Tap">
<Border Height="81" Width="81" CornerRadius="45" BorderBrush="White" BorderThickness="3">
<TextBlock Foreground="White" FontSize="36" Text="&#xE937;" FontFamily="/segmdl2.ttf#Segoe MDL2 Assets" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="0,0,-18,0" />
</Border>
</Button>
<TextBlock x:Name="DurationText" Text="00:00" FontSize="72" FontFamily="Segoe UI Light"/>
</StackPanel>
</Grid>
<Border Grid.Column="1" Background="#121212" BorderThickness="1,0,0,0" BorderBrush="White"/>
<StackPanel Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center">
<Button Visibility="Collapsed" Margin="12" x:Name="CANCEL" Style="{StaticResource ButtonEmptyStyle}" HorizontalAlignment="Left" VerticalAlignment="Bottom" Tap="CANCEL_Tap">
<Border Height="81" Width="81" CornerRadius="45" BorderBrush="White" BorderThickness="3">
<TextBlock Foreground="White" FontSize="36" Text="&#xE10A;" FontFamily="/segmdl2.ttf#Segoe MDL2 Assets" VerticalAlignment="Center" HorizontalAlignment="Center" />
</Border>
</Button>
<Button Visibility="Collapsed" Margin="12" x:Name="REC" Style="{StaticResource ButtonEmptyStyle}" Tap="REC_Tap">
<Border Height="81" Width="81" CornerRadius="45" BorderBrush="White" BorderThickness="3">
<TextBlock Foreground="{StaticResource AppRedColorBrush}" FontSize="36" Text="&#xE91F;" FontFamily="/segmdl2.ttf#Segoe MDL2 Assets" VerticalAlignment="Center" HorizontalAlignment="Center" />
</Border>
</Button>
<Button Visibility="Collapsed" Margin="12" x:Name="STOP" Style="{StaticResource ButtonEmptyStyle}" Tap="STOP_Tap">
<Border Height="81" Width="81" CornerRadius="45" BorderBrush="White" BorderThickness="3" Background="#33E81123">
<TextBlock Foreground="White" FontSize="24" Text="&#xE002;" FontFamily="/segmdl2.ttf#Segoe MDL2 Assets" VerticalAlignment="Center" HorizontalAlignment="Center" />
</Border>
</Button>
<Button Visibility="Collapsed" Margin="12" x:Name="ACCEPT" Style="{StaticResource ButtonEmptyStyle}" HorizontalAlignment="Left" VerticalAlignment="Bottom" Tap="ACCEPT_Tap">
<Border Height="81" Width="81" CornerRadius="45" BorderBrush="White" BorderThickness="3">
<TextBlock Foreground="White" FontSize="36" Text="&#xE10B;" FontFamily="/segmdl2.ttf#Segoe MDL2 Assets" VerticalAlignment="Center" HorizontalAlignment="Center" />
</Border>
</Button>
</StackPanel>
</Grid>
</phone:PhoneApplicationPage>

En mi caso quedo así:


Luego de definir la pantalla, viene el .cs en el cual hay que lidear con el estado de la cámara, no vayamos a usar recursos que ya estan en uso y con la orientación de nuestra aplicación. Para mi fue todo un lío llegar a terminar esta parte, es más, actualmente tiene un bug. Una estrella para quien lo encuentra.

using blog.epicalsoft.com.Phone.Resources;
using Microsoft.Devices;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Tasks;
using System;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Threading;
using Windows.Phone.Media.Capture;
using Windows.Storage;
using Windows.Storage.Streams;
namespace com.epicalsoft.com.Controls
{
public partial class VideoCapturePage : PhoneApplicationPage
{
private Evidence _evidence;
private VideoBrush _viewfinderBrush;
private int _encodedOrientation;
private AudioVideoCaptureDevice _audioVideoCaptureDevice;
private StorageFile _storageFile;
private IRandomAccessStream _randomAccessStream;
private enum ButtonState { Ready, Recording, Recorded, CameraNotSupported };
private TimeSpan _durationTime;
private DispatcherTimer _dispatcherTimer;
public VideoCapturePage()
{
InitializeComponent();
OrientationChanged += VideoCapturePage_OrientationChanged;
}
private void UpdateOrientation()
{
if (Orientation == PageOrientation.LandscapeLeft)
{
CanvasRotateTransform.Angle = 0;
}
else
{
CanvasRotateTransform.Angle = 180;
}
}
private void VideoCapturePage_OrientationChanged(object sender, OrientationChangedEventArgs e)
{
UpdateOrientation();
}
private void UpdateUI(ButtonState currentButtonState)
{
// Run code on the UI thread.
Dispatcher.BeginInvoke(delegate
{
switch (currentButtonState)
{
// When the camera is not supported by the phone.
case ButtonState.CameraNotSupported:
REC.Visibility = Visibility.Collapsed;
STOP.Visibility = Visibility.Collapsed;
PLAY.Visibility = Visibility.Collapsed;
ACCEPT.Visibility = Visibility.Collapsed;
CANCEL.Visibility = Visibility.Collapsed;
break;
// Ready to record, so video is available for viewing.
case ButtonState.Ready:
REC.Visibility = Visibility.Visible;
STOP.Visibility = Visibility.Collapsed;
PLAY.Visibility = Visibility.Collapsed;
ACCEPT.Visibility = Visibility.Collapsed;
CANCEL.Visibility = Visibility.Collapsed;
break;
// Video recording is in progress.
case ButtonState.Recording:
REC.Visibility = Visibility.Collapsed;
STOP.Visibility = Visibility.Visible;
PLAY.Visibility = Visibility.Collapsed;
ACCEPT.Visibility = Visibility.Collapsed;
CANCEL.Visibility = Visibility.Collapsed;
break;
case ButtonState.Recorded:
REC.Visibility = Visibility.Collapsed;
STOP.Visibility = Visibility.Collapsed;
PLAY.Visibility = Visibility.Visible;
ACCEPT.Visibility = Visibility.Visible;
CANCEL.Visibility = Visibility.Visible;
break;
default:
break;
}
});
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
InitializeVideoRecorder();
}
public async void InitializeVideoRecorder()
{
var result = Camera.IsCameraTypeSupported(CameraType.Primary);
if (!result)
{
App.DialogService.ShowToast(AppResources.Msg_NoRearCamera, "Ups!");
return;
}
Dispose();
if (null == _dispatcherTimer)
{
_dispatcherTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_dispatcherTimer.Tick += (s, a) =>
{
_durationTime = _durationTime.Add(TimeSpan.FromSeconds(1));
DurationText.Text = _durationTime.Minutes.ToString("00") + ":" + _durationTime.Seconds.ToString("00");
if (_durationTime.TotalMinutes >= 1.5)
{
StopVideoRecording();
}
};
}
var resolutions = AudioVideoCaptureDevice.GetAvailableCaptureResolutions(CameraSensorLocation.Back);
var minWidth = resolutions.Min(x => x.Width);
Windows.Foundation.Size size = Windows.Foundation.Size.Empty;
size = resolutions.FirstOrDefault(x => x.Width == 320);
if (size == Windows.Foundation.Size.Empty || size.Width == 0)
size = resolutions.First(x => x.Width == minWidth);
_audioVideoCaptureDevice = await AudioVideoCaptureDevice.OpenAsync(CameraSensorLocation.Back, size);
_viewfinderBrush = new VideoBrush();
_viewfinderBrush.SetSource(_audioVideoCaptureDevice);
Canvas.Background = _viewfinderBrush;
if (null == _evidence?.ResSrc)
{
DurationText.Text = "00:00";
_durationTime = TimeSpan.Zero;
ThumbnailImage.Source = null;
UpdateUI(ButtonState.Ready);
}
else
{
UpdateUI(ButtonState.Recorded);
}
}
private async void StartVideoRecording()
{
var isoStore = await ApplicationData.Current.LocalFolder.GetFolderAsync("Shared");
_storageFile = await isoStore.CreateFileAsync("LastIncident.mp4", CreationCollisionOption.ReplaceExisting);
_randomAccessStream = await _storageFile.OpenAsync(FileAccessMode.ReadWrite);
// Initialize variables.
_encodedOrientation = 0;
int sensorOrientation = (int)_audioVideoCaptureDevice.SensorRotationInDegrees;
switch (Orientation)
{
case PageOrientation.LandscapeLeft:
_encodedOrientation = -90 + sensorOrientation;
break;
case PageOrientation.LandscapeRight:
_encodedOrientation = 90 + sensorOrientation;
break;
}
// Apply orientation to image encoding.
_audioVideoCaptureDevice.SetProperty(KnownCameraGeneralProperties.EncodeWithOrientation, _encodedOrientation);
await _audioVideoCaptureDevice.StartRecordingToStreamAsync(_randomAccessStream);
UpdateOrientation();
UpdateUI(ButtonState.Recording);
using (var tnMS = new MemoryStream())
{
var wb = new WriteableBitmap(Canvas, null);
if (Orientation == PageOrientation.LandscapeRight)
wb = wb.Rotate(_encodedOrientation);
wb.SaveJpeg(tnMS, wb.PixelWidth, wb.PixelHeight, 0, 72);
tnMS.Seek(0, SeekOrigin.Begin);
_evidence = new Evidence { Kind = EvidenceKind.Video };
_evidence.TnSrc = Convert.ToBase64String(tnMS.ToArray());
_evidence.TnExt = "jpg";
}
}
private async void StopVideoRecording()
{
_dispatcherTimer.Stop();
STOP.Visibility = Visibility.Collapsed;
await _audioVideoCaptureDevice.StopRecordingAsync();
UpdateOrientation();
var sfStream = await _storageFile.OpenStreamForReadAsync();
var resStream = new MemoryStream();
sfStream.CopyTo(resStream);
var bitmapImage = new BitmapImage();
var stream = new MemoryStream(Convert.FromBase64String(_evidence.TnSrc));
bitmapImage.SetSource(stream);
ThumbnailImage.Source = bitmapImage;
_evidence.ResSrc = Convert.ToBase64String(resStream.ToArray());
_evidence.ResExt = "mp4";
_evidence.Data = _storageFile.Path;
sfStream.Dispose();
resStream.Dispose();
stream.Dispose();
UpdateUI(ButtonState.Recorded);
}
#region Dispose
protected override void OnBackKeyPress(CancelEventArgs e)
{
_storageFile?.DeleteAsync();
_storageFile = null;
Dispose();
}
private void Dispose()
{
_dispatcherTimer?.Stop();
_audioVideoCaptureDevice?.Dispose();
_randomAccessStream?.Dispose();
_dispatcherTimer = null;
_viewfinderBrush = null;
_audioVideoCaptureDevice = null;
_randomAccessStream = null;
}
#endregion Dispose
#region Events
private void REC_Tap(object sender, System.Windows.Input.GestureEventArgs e)
{
_dispatcherTimer.Start();
REC.Visibility = Visibility.Collapsed;
StartVideoRecording();
}
private void STOP_Tap(object sender, System.Windows.Input.GestureEventArgs e)
{
StopVideoRecording();
}
private void PLAY_Tap(object sender, System.Windows.Input.GestureEventArgs e)
{
new MediaPlayerLauncher()
{
Media = new Uri((string)_evidence.Data, UriKind.Relative),
Location = MediaLocationType.Data,
Controls = MediaPlaybackControls.All,
Orientation = MediaPlayerOrientation.Landscape,
}.Show();
}
private void ACCEPT_Tap(object sender, System.Windows.Input.GestureEventArgs e)
{
Dispose();
App.PendingVideo = _evidence;
NavigationService.GoBack();
}
private void CANCEL_Tap(object sender, System.Windows.Input.GestureEventArgs e)
{
_evidence = new Evidence { Kind = EvidenceKind.Video };
InitializeVideoRecorder();
}
#endregion Events
}
}

En fin, luego de haber capturado correctamente la información y dando aceptar, nos ayudamos de una variable estática en el App.xaml.cs llamada PendingVideo y navegamos hacia atrás. Ya del otro lado, la usamos y limpiamos referencias innecesarias.

protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (null != App.PendingVideo)
{
//TODO
App.PendingVideo = null;
}
}

No hay comentarios.:

Publicar un comentario

Epicalsoft — Superheroic Software Development Blog Designed by Templateism Copyright © 2014

Con tecnología de Blogger.