FC2ブログ

記事一覧

動画を撮影(録画)してファイルとして保存する方法 | Xamarin.Forms


以前の記事ではアプリ内で動画を再生する方法をご紹介しましたが、今回は Xamarin.Forms でカメラを使用して動画を撮影し、プレビュー画面を表示しながら録画し、ファイルとして保存する方法をご紹介いたします。Android と iOS でカメラを操作するネイティブライブラリが異なるため、DependencyServiceで実装します。動画を撮影して保存するだけなのですが、結構なソースコード量が必要となりますので、実装には時間がかかりました。


前提条件
・Windows10 Pro 64Bit 1709
・Visual Studio 2015 Community Update3
・Xamarin 4.8.0.760 (NuGet Xamarin.Forms 2.4.0.282)
・macOS Sierra 10.12.6 / Xcode9 / Xamarin.iOS 11.6.1.4



1.PCLの記述方法

PCL プロジェクト内に DependencyService で呼び出すためのインターフェースを配置します。

IVideoService.cs
namespace AppName.Services
{
//DependencyServiceから利用する
public interface IVideoService
{
void PrepareRecord(string saveFilePath);
void StartRecord();
void StopRecord();
}
}



2.Androidの実装方法

(1)AndroidManifest.xml に以下のパーミッションとハードウェアアクセラレーションを有効にする設定を追加します。
  <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application android:hardwareAccelerated="true" />

※application のタグは2つ以上含めてはいけません。既に AndroidManifest.xml に application タグが存在する場合は、android:hardwareAccelerated="true" のみ追記してください。


(2)Android プロジェクト内に以下のクラス群を実装します。
※Android5.0 以降で Camera API2 を使用する必要がある為、Android5 よりも前のバージョンの動作は対象外です。

VideoService.cs
using System;
using System.Threading.Tasks;
using Android.App;
using Android.Views;
using Android.Widget;
using Android.Media;
using Xamarin.Forms;
using AppName.Droid.Services;
using AppName.Services;
[assembly: Dependency(typeof(VideoService))]
namespace AppName.Droid.Services
{
public class VideoService : IVideoService {
#region "録画"

private static MediaRecorder _recorder = null;
private static LinearLayout _linearLayout = null;
private static TextureView _textureView = null;
private static bool _isRecording = false;

public void PrepareRecord(string saveFilePath)
{
//MediaRecorderを設定します。
this.SetUpMediaRecorder(saveFilePath);

// MediaRecorderのプレビュー用のSurfaceViewを作成する
var context = Forms.Context;

//入力項目を格納するレイアウト
_linearLayout = new LinearLayout(context);
_linearLayout.LayoutParameters = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MatchParent,
ViewGroup.LayoutParams.MatchParent);
_linearLayout.SetBackgroundColor(Android.Graphics.Color.White);
((MainActivity)context).AddContentView(_linearLayout,
new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WrapContent,
ViewGroup.LayoutParams.WrapContent));
_textureView = new TextureView(context);
_textureView.SurfaceTextureListener = new SurfaceTextureListener();
_linearLayout.AddView(_textureView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.WrapContent));
}

private void SetUpMediaRecorder(string saveFilePath)
{
if (_recorder == null)
{
//MediaRecorderの設定
_recorder = new MediaRecorder();

// 入力ソースの設定
_recorder.SetVideoSource(VideoSource.Surface); // 録画の入力ソースを指定
_recorder.SetAudioSource(AudioSource.Mic); // 音声の入力ソースを指定

// ファイルフォーマットの設定
_recorder.SetOutputFormat(OutputFormat.ThreeGpp); // ファイルフォーマットを指定

// エンコーダーの設定
//_recorder.SetVideoEncoder(VideoEncoder.Mpeg4Sp); // ビデオエンコーダを指定
//_recorder.SetAudioEncoder(AudioEncoder.AmrNb); // オーディオエンコーダを指定
_recorder.SetVideoEncoder(VideoEncoder.H264); // ビデオエンコーダを指定
_recorder.SetAudioEncoder(AudioEncoder.Aac); // オーディオエンコーダを指定

// 各種設定
_recorder.SetOutputFile(saveFilePath); // 動画の出力先となるファイルパスを指定
_recorder.SetVideoEncodingBitRate(10000000);
_recorder.SetVideoFrameRate(29); //信号機の点滅レートも30  動画のフレームレートを指定
_recorder.SetVideoSize(320, 240); // 動画のサイズを指定

_recorder.Prepare(); // 録画準備
}
}


public static void OpenCamera2(SurfaceTexture surfaceTexture)
{
//Camera2
CameraManager manager = (CameraManager)Android.App.Application.Context.GetSystemService(Context.CameraService);
string cameraId = manager.GetCameraIdList().FirstOrDefault();
CameraCharacteristics cameraCharacteristics = manager.GetCameraCharacteristics(cameraId);
Android.Hardware.Camera2.Params.StreamConfigurationMap scm = (Android.Hardware.Camera2.Params.StreamConfigurationMap)cameraCharacteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap);
var previewSize = scm.GetOutputSizes((int)ImageFormatType.Jpeg)[0];
manager.OpenCamera(cameraId, new CameraCallBack(_recorder, surfaceTexture, previewSize), null);
}


public void StartRecord()
{
if (_recorder != null)
{
_textureView.Visibility = ViewStates.Visible;

// 録画開始
_recorder.Start();

_isRecording = true;
}
}

public void StopRecord()
{
if (_isRecording)
{
// 録画終了
_recorder.Stop();
_recorder.Reset();
_recorder.Release();

if (_linearLayout != null)
{
((ViewGroup)_linearLayout.Parent).RemoveView(_linearLayout);
_linearLayout.Dispose();
_linearLayout = null;
}

_isRecording = false;
}
}

#endregion
}

public class SurfaceTextureListener : Java.Lang.Object, TextureView.ISurfaceTextureListener
{
#region "TextureView"

public void OnSurfaceTextureAvailable(SurfaceTexture surface, int width, int height)
{
VideoService.OpenCamera2(surface);
}

public bool OnSurfaceTextureDestroyed(SurfaceTexture surface)
{
return true;
}

public void OnSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height)
{
//throw new NotImplementedException();
}

public void OnSurfaceTextureUpdated(SurfaceTexture surface)
{
//throw new NotImplementedException();
}
#endregion

}

public class CameraCallBack : CameraDevice.StateCallback
{
private MediaRecorder _recorder = null;
private CameraDevice _cameraDevice = null;
private SurfaceTexture _surfaceTexture = null;
private CaptureRequest _captureRequest = null;
private Android.Util.Size _previewSize = null;
public CameraCallBack(MediaRecorder recorder, SurfaceTexture surfaceTexture, Android.Util.Size previewSize)
{
_recorder = recorder;
_surfaceTexture = surfaceTexture;
_previewSize = previewSize;
}

public override void OnDisconnected(CameraDevice camera)
{
_cameraDevice.Close();
_cameraDevice = null;
}

public override void OnError(CameraDevice camera, Android.Hardware.Camera2.CameraError error)
{
LogUtility.OutPutError(error.ToString());
_cameraDevice.Close();
_cameraDevice = null;
}

public override void OnOpened(CameraDevice camera)
{
_cameraDevice = camera;
this.CreateCaptureSession();
}

private void CreateCaptureSession()
{
//SurfaceTexture texture = _textureView.SurfaceTexture; //エラーになる
//バッファのサイズをプレビューサイズに設定(画面サイズ等適当な値を入れる)
_surfaceTexture.SetDefaultBufferSize(_previewSize.Width, _previewSize.Height);
Surface surface = new Surface(_surfaceTexture);

List<Surface> list = new List<Surface>();
list.Add(surface);
list.Add(_recorder.Surface);

CaptureRequest.Builder captureRequest = _cameraDevice.CreateCaptureRequest(CameraTemplate.Record);
captureRequest.AddTarget(surface);
captureRequest.AddTarget(_recorder.Surface);
_captureRequest = captureRequest.Build();
_cameraDevice.CreateCaptureSession(list, new CameraCaputureSessionCallBack(_captureRequest), null);
}
}

//キャプチャセッションの状態取得
public class CameraCaputureSessionCallBack : CameraCaptureSession.StateCallback
{
private CaptureRequest _captureRequest = null;

public CameraCaputureSessionCallBack(CaptureRequest captureRequest)
{
_captureRequest = captureRequest;
}

public override void OnConfigured(CameraCaptureSession session)
{
session.SetRepeatingRequest(_captureRequest, new CameraCaptureSessionCallBack(), null);
//session.StopRepeating();
session.Capture(_captureRequest, new CameraCaptureSessionCallBack(), null);
}

public override void OnConfigureFailed(CameraCaptureSession session)
{
//throw new NotImplementedException();
}
}

//キャプチャー開始
public class CameraCaptureSessionCallBack : CameraCaptureSession.CaptureCallback
{
public override void OnCaptureStarted(CameraCaptureSession session, CaptureRequest request, long timestamp, long frameNumber)
{
base.OnCaptureStarted(session, request, timestamp, frameNumber);
}

public override void OnCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result)
{
base.OnCaptureCompleted(session, request, result);
}
}
}

※一時停止やボリューム・停止・再生などの操作ボタンは表示されません。
 必要な操作があればソースを追加で記述してください。
※撮影できる拡張子には制限があるようです。
 撮影可能拡張子:3gp, mp4
 撮影不可拡張子:wmv, flv



3.iOSの実装方法

(1)Info.plist ファイルに以下のパーミッションを追加します。
<plist version="1.0">
<dict>
  <key>NSCameraUsageDescription</key>
  <string>ビデオ撮影のためにカメラを使用します。</string>
<key>NSMicrophoneUsageDescription</key>
<string>ビデオ撮影のためにマイクを使用します。</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>ビデオ撮影を保存するためにフォトライブラリを利用します。</string>
</dict>
</plist>


(2)iOS プロジェクト内に以下のクラスを配置します。

VideoService.cs
using System;
using System.IO;
using Foundation;
using UIKit;
using AVFoundation;
using AVKit;
using CoreGraphics;
using Xamarin.Forms;
using AppName.iOS.Services;
using AppName.Services;
[assembly: Dependency(typeof(VideoService))]
namespace AppName.iOS.Services
{
public class VideoService : IVideoService
{
private CaptureController _captureController = null;
private UINavigationController _viewController = null;
public void PrepareRecord(string saveFilePath)
{
_captureController = new CaptureController(saveFilePath);

_viewController = UIApplication.SharedApplication.KeyWindow.RootViewController;
while (viewController.PresentedViewController != null)
{
_viewController = viewController.PresentedViewController;
}
_viewController.PresentViewController(_captureController, true, () =>
{

});
}

public void StartRecord()
{
_captureController.Start();
}

public void StopRecord()
{
_captureController.Stop();
_viewController.DismissViewController(true, null);
_captureController.Dispose();
_captureController = null;
}
}

public class CaptureController : UIViewController, IAVCaptureFileOutputRecordingDelegate, IAVCapturePhotoCaptureDelegate
{
private AVCaptureSession _captureSession = null;
private AVCaptureMovieFileOutput _movieOutput = null;
private AVCaptureVideoPreviewLayer _videoLayer = null;
private bool _isRecording = false;
private NSUrl _url = null;

public CaptureController(string saveFilePath) : base()
{
_url = NSUrl.FromFilename(saveFilePath);
}

public override void ViewDidLoad()
{
base.ViewDidLoad();

// セッションのインスタンス化
_captureSession = new AVCaptureSession();

// デバイスの初期化
AVCaptureDevice videoDevice = AVCaptureDevice.GetDefaultDevice(AVMediaType.Video); // カメラ
AVCaptureDevice audioDevice = AVCaptureDevice.GetDefaultDevice(AVMediaType.Audio); // マイク

// デバイスの接続
AVCaptureDeviceInput videoInput = AVCaptureDeviceInput.FromDevice(videoDevice);
AVCaptureDeviceInput audioInput = AVCaptureDeviceInput.FromDevice(audioDevice);
_captureSession.AddInput(videoInput);
_captureSession.AddInput(audioInput);

// 出力の初期化
_fileOutput = new AVCaptureMovieFileOutput(); // 映像ファイル

// 出力の接続
if (_captureSession.CanAddOutput(_movieOutput))
{
_captureSession.AddOutput(_movieOutput);
}

// キャプチャの品質レベル、ビットレートなどのクオリティを設定
this.ConfigureSession();

_videoLayer = new AVCaptureVideoPreviewLayer(_captureSession);
_videoLayer.Frame = this.View.Bounds;
_videoLayer.VideoGravity = AVLayerVideoGravity.ResizeAspectFill;
this.View.Layer.AddSublayer(_videoLayer);
}
public override bool ShouldAutorotateToInterfaceOrientation(UIInterfaceOrientation toInterfaceOrientation)
{
// Return true for supported orientations
return (toInterfaceOrientation != UIInterfaceOrientation.PortraitUpsideDown);
}

public void Start()
{
if (_captureSession == null)
{
return;
}
var task = new System.Threading.Tasks.Task(() =>
{
// セッション開始
_captureSession.StartRunning();
// 録画開始
_fileOutput.StartRecordingToOutputFile(_url, this);
});

task.Start();
_isRecording = true;
}

public void Stop()
{
if (!_isRecording)
{
return;
}

//レイヤーを取り除く
if (_videoLayer != null)
{
_videoLayer.RemoveFromSuperLayer();
}

// 録画終了
_fileOutput.StopRecording();
// セッション終了
_captureSession.StopRunning();
_captureSession.Dispose();
_captureSession = null;
_isRecording = false;
}

public void ConfigureSession()
{
if (_captureSession == null)
{
return;
}

if (_isRecording)
{
_captureSession.BeginConfiguration();
}

// キャプチャの品質レベル、ビットレートなどのクオリティを設定
if (_captureSession.CanSetSessionPreset(AVCaptureSession.PresetHigh))
{
_captureSession.SessionPreset = AVCaptureSession.PresetHigh;
}
else
{
_captureSession.SessionPreset = AVCaptureSession.PresetMedium;
}

if (_isRecording)
{
_captureSession.CommitConfiguration();
}
}

public void FinishedRecording(AVCaptureFileOutput captureOutput, NSUrl outputFileUrl, NSObject[] connections, NSError error)
{
//iOS9
//var assetsLib = new ALAssetsLibrary();
//assetsLib.WriteVideoToSavedPhotosAlbum(_url, null);

//iOS10移行
if (UIDevice.CurrentDevice.CheckSystemVersion(10, 0))
{
if (Photos.PHPhotoLibrary.AuthorizationStatus == Photos.PHAuthorizationStatus.NotDetermined ||
Photos.PHPhotoLibrary.AuthorizationStatus == Photos.PHAuthorizationStatus.Authorized)
{
Photos.PHPhotoLibrary.SharedPhotoLibrary.PerformChanges(() =>
{
Photos.PHAssetChangeRequest.FromVideo(outputFileUrl);
}, (success, err) =>
{
if (!success)
{
Debug.WriteLine(err.LocalizedDescription + System.Environment.NewLine + err.LocalizedFailureReason);
}
});
}
}
}
}
}

※撮影できる拡張子には制限があるようです。
撮影可能拡張子:mp4
撮影不可拡張子:wmv, flv



4.使用方法

PCLプロジェクトの中の任意のページに記述します。

TestPage.xaml.cs
void StartRecordTest(object sender, EventArgs e)
{
    string extension = ".3gp";
    if (Device.RuntimePlatform == Device.iOS)
    {
        extension = ".mp4";
    }

    DependencyService.Get<IVideoService>().PrepareRecord(Common.GetDocumentPath() + "/Record_" + DateTimeOffset.Now.LocalDateTime.ToString("yyyyMMddHHmmss") + extension);
    DependencyService.Get<IVideoService>().StartRecord();
           
    var task = new Task(() =>
    {
        //20秒撮影している間待機
        Task.Delay(20000).Wait();

        //停止時にUIを操作する為、Device.BeginInvokeOnMainThreadで囲みます
        Device.BeginInvokeOnMainThread(() =>
        {
            //撮影を停止します。
            DependencyService.Get<IVideoService>().StopRecord();
        });
    });

    task.Start();
}

※ PrepareRecord の引数にはディレクトリを含めたファイルパスを渡します。






最後までお読みいただきありがとうございます。
当ブログの内容をまとめた Xamarin逆引きメニュー は以下のURLからご覧になれます。
https://itblogdsi.blog.fc2.com/blog-entry-81.html




関連記事

コメント

No title
同じようなことを実現しようとネットに潜ってたどり着きました。
Android側の実装も期待してます!※私は途中まで書いて挫折しました(笑)
ところでiOSのサンプルコードに不備があるように見受けられます。
 ・_movieOutputが定義されていない
 ・_viewControllerの型が不正?UIViewControllerかな?
 ・new CaptureController(string saveFilePath)の「string」が不要
 ・「Recode」は「Record」のtypoかな?
更新楽しみにしてます!
Re: No title
コメントありがとうございます。励みになります。

AndroidはcameraAPI2を使用することが必要だということがわかりましたので変更して実装まではできましたが、録画でつまづいています。がんばって今月中には実装完了したいと考えています。

recodeはタイプミスですね。大変失礼しました。
Re: Re: No title
お待たせしました。
Android の動画撮影方法を CameraAPI2 と MediaRecord で実装したソースで更新しました。
コメントの投稿

カテゴリ別記事一覧

広告

プロフィール

石河 純


著者名 :石河 純
自己紹介:素人上がりのIT技術者。趣味は卓球・車・ボウリング

IT関連の知識はざっくりとこんな感じです。
【OS関連】
WindowsServer: 2012/2008R2/2003/2000/NT4
Windows: 10/8/7/XP/2000/me/NT4/98
Linux: CentOS RedHatLinux9
Mac: macOS Sierra 10.12 / OSX Lion 10.7.5 / OSX Snow Leopard 10.6.8
【言語・データベース】
VB.net ASP.NET C#.net Java VBA
Xamarin.Forms
Oracle10g SQLServer2008R2 SQLAnywhere8/11/16
ActiveReport CrystalReport ReportNet(IBM)
【ネットワーク関連】
CCNP シスコ技術者認定
Cisco Catalyst シリーズ
Yamaha RTXシリーズ
FireWall関連
【WEB関連】
SEO SEM CSS IIS6/7 apache2

休みの日は卓球をやっています。
現在、卓球用品通販ショップは休業中です。