UnityでAndroidのCameraX APIを利用して動画撮影する
はじめに
今回は Android の CameraX API を用いて、Unity アプリにカメラ撮影機能を実装する方法について書きたいと思います。
これを実装した背景は、Google が主催する Gemini を利用したコンペに応募するためのアプリ開発に必要だったからです。
実際に動かしてみた動画は以下です。Unity アプリが起動し、画面にはカメラのプレビューが表示されていませんが、実際に動画撮影が出来ていることが分かります。
Android Studio を利用した通常の Android アプリでは実装はとても簡単なのですが、ライブラリ化しつつ Unity のライフサイクルなどに合わせて実装するのに少し手間取ったので備忘録も兼ねて書きたいと思います。
Android Studio プロジェクトの設定
まずは Android Studio で CameraX API を利用した Android アプリ(ライブラリ)を作成します。そのための設定について書きます。
AnroidManifest.xml の設定
カメラを利用するため、以下の設定を <manifest>
タグの中に追加します。
<manifest ...>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- 後略 -->
</manifest>
Gradle の設定
ライブラリ開発をするにあたり、まずは Android Studio での Gradle の設定を行います。今回は CameraX API を利用するため以下の依存関係を追加します。
dependencies {
implementation(libs.androidx.constraintlayout)
implementation(project(":CameraRecorder"))
val camerax_version = "1.1.0-beta01"
implementation("androidx.camera:camera-core:${camerax_version}")
implementation("androidx.camera:camera-camera2:${camerax_version}")
implementation("androidx.camera:camera-lifecycle:${camerax_version}")
implementation("androidx.camera:camera-video:${camerax_version}")
implementation("androidx.camera:camera-view:${camerax_version}")
implementation("androidx.camera:camera-extensions:${camerax_version}")
// ... 後略
}
gradle.properties の設定
CameraX を利用することを伝えるため、 gradle.properties
に以下の設定を追加します。
android.useAndroidX=true
AAR 化するためのモジュールを追加
新規作成した Android プロジェクトに、新規にモジュールを追加します。このモジュールを AAR 化し、Unity で利用します。
[File] > [New] > [New Module]
から追加画面を表示し、 Android Library を選択して項目を設定します。
開いた画面で Android Library
を選択します。各項目はそれぞれ適切に設定してください。
プロジェクトにモジュールの依存関係を追加する
本体アプリ側から、追加したモジュールのコードを利用する場合は Project Structure...
から依存を追加する必要があります。
CameraX API を利用するライブラリの実装
まずは実装するファイルをモジュールに追加します。(ここでは CameraRecorder.kt
を追加しています)
CameraRecorder クラスの実装
実装する CameraRecorder
クラスは Activity
を受け取り、LifecycleOwner
インターフェースを実装します。Activity
を受け取るのは Activity
からでしか利用できないものがあるため、インスタンス化する際に受け取ります。
シグネチャは以下の通り。
class CameraRecorder(
private val activity: Activity,
private val targetName: String,
private val cameraFacing: Int = 0, // 0 = Front, 1 = Back
) : LifecycleOwner {
override val lifecycle: Lifecycle
get() = lifecycleRegistry
}
Activity
は UnityPlayerActivity
を受け取る想定です。また、カメラの起動・終了などのライフサイクルを LifecycleOwner
のイベントで管理するため、このインターフェースを実装しています。
このインターフェースは Lifecycle
を取得する lifecycle
プロパティを要求します。
public val lifecycle: Lifecycle
ライフサイクルの管理
今回の実装では、ライフサイクルのタイミングを以下のようにしています。
- コンストラクタ実行時に
ON_CREATE
イベントを発行 -
startCapture
メソッドでON_START
イベントを発行 -
stopCapture
メソッドでON_STOP
イベントを発行
イベントの通知には以下のように handleLifecycleEvent
メソッドを利用します。
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
カメラの起動
カメラの起動には ProcessCameraProvider
を利用します。また、起動時には「Usecase」として任意の数のユースケースオブジェクトを渡すことができます。
これは、例えばプレビュー UI にカメラの映像を表示したり、あるいは ImageCapture
などの、画像取得のためのユースケースなど複数あります。今回は動画撮影をするため VideoCapture
クラスを利用します。
private fun startCapture() {
// メインスレッドに切り替えて処理を行う
Handler(Looper.getMainLooper()).post {
Log.d(TAG, "Start camera with target $targetName")
// ライフサイクルの ON_START イベントを発行
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
// ProcessCameraProvider のインスタンスを取得
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Recorder オブジェクトを生成し、VideoCapture の output に設定
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST))
.build()
videoCapture = VideoCapture.withOutput(recorder)
val cameraSelector = if (cameraFacing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(this, cameraSelector, videoCapture)
// 起動と同時に動画撮影を開始する
captureVideo()
}
catch (exc: Exception) {
Log.e(TAG, "Use case binding failed.", exc)
}
}
}
cameraProvider
の bindToLifecycle
メソッドでライフサイクルオーナーとして自身と、撮影に使うカメラの方向(Front / Back)およびユースケースを指定してカメラを起動します。
動画撮影の開始
動画撮影には Recorder
クラスを利用します。なお、今回は MediaStoreOutputOptions
を利用して動画を保存するためギャラリー内に保存されます。
カメラ起動時に videoCapture.withOutput(recorder)
で Recorder
オブジェクトを指定しています。これは videoCapture.output
プロパティで参照できます。
このプロパティを通して動画撮影の準備および開始を行います。
また、今回は音声も同時に録音するため、recorder.withAudioEnabled()
を呼び出しています。
録画の開始は start
メソッドを実行します。引数にはラムダ式を渡し、中では VideoRecordEvent
を受け取ります。これは、撮影の各種イベントを受け取るコールバックです。
特に重要なのは VideoRecordEvent.Finalize
イベントで、このイベント内でファイルの場所を取得して Unity に通知します。
recording = videoCapture.output
.prepareRecording(context, mediaStoreOutputOptions)
.apply {
if (PermissionChecker.checkSelfPermission(this@CameraRecorder.context,
Manifest.permission.RECORD_AUDIO) == PermissionChecker.PERMISSION_GRANTED) {
withAudioEnabled()
}
}
.start(ContextCompat.getMainExecutor(context)) { recordEvent ->
when (recordEvent) {
is VideoRecordEvent.Start -> {
startVideoCaptureEvent(Unit)
}
is VideoRecordEvent.Finalize -> {
// NOTE: なぜか hasError が true になるが、値は正常に保持しているため、チェックしない
// if (recordEvent.hasError())
// {
// recording?.close()
// recording = null
// Log.e(TAG, "Video capture ends with error: ${recordEvent.error}")
//
// finishVideoCaptureEvent("")
// }
// else {
val msg = "Video capture succeeded: ${recordEvent.outputResults.outputUri}"
Log.d(TAG, msg)
var cursor: Cursor? = null
try {
val proj = arrayOf(MediaStore.Video.Media.DATA)
cursor = context.contentResolver.query(recordEvent.outputResults.outputUri, proj, null, null, null)
if (cursor != null)
{
val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)
cursor.moveToFirst()
val path = cursor.getString(columnIndex)
// Toast.makeText(activity.baseContext, path, Toast.LENGTH_SHORT)
// .show()
Log.d(TAG, "Saved video at [$path]")
notifyCapturedEventToUnity(path)
}
}
catch (exc: Exception) {
cursor?.close()
}
finally {
cursor?.close()
}
// }
}
}
}
ファイルの保存場所の取得
Finalize イベント内で取得できる recordEvent.outputResults.outputUri
は動画ファイルの URI です。しかし、前述のように今回の保存先には MediaStore
を利用して保存しているため、この URI は content://...
のようなパスとなり実際の動画のファイルパスではありません。これを、Unity から利用できるように動画の実際のファイルパスを検索して返すようにしています。
具体的には以下の部分です。
var cursor: Cursor? = null
try {
val proj = arrayOf(MediaStore.Video.Media.DATA)
cursor = context.contentResolver.query(recordEvent.outputResults.outputUri, proj, null, null, null)
if (cursor != null)
{
val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)
cursor.moveToFirst()
val path = cursor.getString(columnIndex)
Log.d(TAG, "Saved video at [$path]")
notifyCapturedEventToUnity(path)
}
}
catch (exc: Exception) {
cursor?.close()
}
finally {
cursor?.close()
}
まず最初に MediaStore.Video.Media.DATA
からビデオディレクトリのファイルパスを取得し、context.contentResolver.query
を使ってファイルパスを取得します。
全体の流れとしては、ビデオが保存されているパスの絶対パスを取得し、そこから contentResolver
に対してクエリを投げて、対象の位置(カーソル)を取得します。
そして取得したカーソルから、対象動画の絶対パスを取得しています。
動画撮影の終了
最後に、動画撮影の終了処理です。動画撮影の終了もメインスレッドから行う必要があります。
ライフサイクルとして ON_STOP
イベントを発行し、さらにカメラのバインドを解除します。
また Record
オブジェクトの stop
メソッドを呼び出します。
これを実行すると撮影が終了し、動画ファイルが保存されます。
動画保存が完了すると前述の VideoRecordEvent.Finalize
イベントが発生します。
private fun stopCapture() {
Log.d(TAG, "CameraRecorder is stopping video capturing.")
Handler(Looper.getMainLooper()).post {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
recording?.stop()
recording = null
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
cameraProvider.unbindAll()
}
}
大まかな流れは以上です。実装自体はそこまで複雑ではありません。次はこれを AAR (ライブラリ)としてビルドし、Unity で利用できる形にします。
Unity の機能を参照できるようにする
ライブラリ化にあたり、イベントの開始・修了や、保存が完了した動画のファイルパスなどを Unity 側に通知する必要があります。今回は Unity が提供してくれている UnityPlayer.UnitySendMessage()
を利用して通知します。
Unity の classes.jar の追加
Unity の classes.jar
ファイルをプロジェクトに追加します。該当のファイルは以下のような場所に、Unity のバージョンごとに保存されています。
上記の jar ファイルをモジュール内の libs
ディレクトリにコピーします。(存在しない場合は作成してください)
Gradle の設定
これを利用するため、Unity が提供してくれている jar ファイルを参照できるようにします。ここでひとつ注意点があります。Unity が提供している jar ファイルは classes.jar
という名前で、これ自体は Unity がビルドしたアプリ内に存在するため、今回の AAR 化の際に、この jar ファイルを含めてしまうと重複してしまうため、コンパイル時だけ参照するように設定しておく必要があります。
モジュール側の build.gradle
に以下の設定を追加します。
dependencies {
compileOnly(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
// ... 後略
}
最後に、今回実装したコード全文を載せておきます。不明な点は以下を参照ください。
コード全文
package tokyo.meson.camerarecorder
import android.Manifest
import android.app.Activity
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.graphics.Bitmap
import android.media.MediaMetadataRetriever
import android.os.Build
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import android.util.Log
import android.widget.Toast
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.MediaStoreOutputOptions
import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector
import androidx.camera.video.Recorder
import androidx.camera.video.Recording
import androidx.camera.video.VideoCapture
import androidx.camera.video.VideoRecordEvent
import androidx.core.content.ContextCompat
import androidx.core.content.PermissionChecker
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import com.unity3d.player.UnityPlayer
import java.text.SimpleDateFormat
import java.util.Locale
import java.io.File
import java.io.FileOutputStream
import kotlin.concurrent.thread
class CameraRecorder(
private val activity: Activity,
private val targetName: String,
private val cameraFacing: Int = 0, // 0 = Front, 1 = Back
) : LifecycleOwner {
companion object {
private const val TAG = "CameraRecorder"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
}
private val context: Context = activity
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
private var imageCapture: ImageCapture? = null
private var videoCapture: VideoCapture<Recorder>? = null
private var recording: Recording? = null
val startVideoCaptureEvent = CameraRecorderEvent<Unit>()
val finishVideoCaptureEvent = CameraRecorderEvent<String>()
val finishGetFrameEvent = CameraRecorderEvent<String>()
override val lifecycle: Lifecycle
get() = lifecycleRegistry
val isRecording: Boolean
get() = recording != null
init {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
private fun startCapture() {
Handler(Looper.getMainLooper()).post {
Log.d(TAG, "Start camera with target $targetName")
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
// ProcessCameraProvider のインスタンスを取得
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST))
.build()
videoCapture = VideoCapture.withOutput(recorder)
val cameraSelector = if (cameraFacing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(this, cameraSelector, videoCapture)
captureVideo()
}
catch (exc: Exception) {
Log.e(TAG, "Use case binding failed.", exc)
}
}
}
private fun stopCapture() {
Log.d(TAG, "CameraRecorder is stopping video capturing.")
Handler(Looper.getMainLooper()).post {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
recording?.stop()
recording = null
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
cameraProvider.unbindAll()
}
}
private fun captureVideo() {
Log.d(TAG, "CameraRecorder will start or stop video capturing.")
val videoCapture = this.videoCapture ?: return
val curRecording = recording
if (curRecording != null) {
stopCapture()
return
}
Log.d(TAG, "CameraRecorder is starting video capturing.")
// Create and start a new recording session
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
.format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
}
}
val mediaStoreOutputOptions = MediaStoreOutputOptions
.Builder(activity.contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
.setContentValues(contentValues)
.build()
recording = videoCapture.output
.prepareRecording(context, mediaStoreOutputOptions)
.apply {
if (PermissionChecker.checkSelfPermission(this@CameraRecorder.context,
Manifest.permission.RECORD_AUDIO) == PermissionChecker.PERMISSION_GRANTED) {
withAudioEnabled()
}
}
.start(ContextCompat.getMainExecutor(context)) { recordEvent ->
when (recordEvent) {
is VideoRecordEvent.Start -> {
startVideoCaptureEvent(Unit)
}
is VideoRecordEvent.Finalize -> {
// NOTE: なぜか hasError が true になるが、値は正常に保持しているため、チェックしない
// if (recordEvent.hasError())
// {
// recording?.close()
// recording = null
// Log.e(TAG, "Video capture ends with error: ${recordEvent.error}")
//
// finishVideoCaptureEvent("")
// }
// else {
val msg = "Video capture succeeded: ${recordEvent.outputResults.outputUri}"
Log.d(TAG, msg)
var cursor: Cursor? = null
try {
val proj = arrayOf(MediaStore.Video.Media.DATA)
cursor = context.contentResolver.query(recordEvent.outputResults.outputUri, proj, null, null, null)
if (cursor != null)
{
val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)
cursor.moveToFirst()
val path = cursor.getString(columnIndex)
// Toast.makeText(activity.baseContext, path, Toast.LENGTH_SHORT)
// .show()
Log.d(TAG, "Saved video at [$path]")
notifyCapturedEventToUnity(path)
}
}
catch (exc: Exception) {
cursor?.close()
}
finally {
cursor?.close()
}
// }
}
}
}
}
fun start() {
startCapture()
}
fun stop() {
stopCapture()
}
fun getFrameAtTime(filePath: String, millseconds: Int) {
thread {
val retriever = MediaMetadataRetriever().apply {
setDataSource(filePath)
}
val timeUs: Long = millseconds.toLong() * 1000L
val bitmap = retriever.getFrameAtTime(timeUs, MediaMetadataRetriever.OPTION_NEXT_SYNC)
bitmap ?: return@thread
val filename = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
.format(System.currentTimeMillis())
val file = File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), "${filename}.png")
val fileOutputStream = FileOutputStream(file)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream)
fileOutputStream.flush()
notifyCreatedFrameEventToUnity(file.path)
}
}
fun deleteFile(filePath: String) {
val file = File(filePath)
file.delete()
}
private fun notifyCapturedEventToUnity(filePath: String) {
Log.d(TAG, "Will send the video file path to Unity. [$filePath]")
finishVideoCaptureEvent(filePath)
UnityPlayer.UnitySendMessage(targetName, "CapturedVideo", filePath)
}
private fun notifyCreatedFrameEventToUnity(filePath: String) {
Log.d(TAG, "Will send the image file path to Unity. [$filePath]")
finishGetFrameEvent(filePath)
UnityPlayer.UnitySendMessage(targetName, "CreatedFrame", filePath)
}
}
AAR 化する
上記の CameraRecorder
クラスを AAR ライブラリとしてビルドします。
メニューの [Build] > [Make Module '<MODULE_NAME>']
から対象プロジェクトをビルドします。
ビルドが正常に終了すると、build/outputs/aar
ディレクトリに AAR ファイルが生成されます。
Unity 側の実装
ビルドした AAR ファイルを Unity プロジェクト内の Plugins フォルダにコピーします。また、CameraX API を利用するためカスタムの Manifest ファイルなどを追加し設定する必要があります。
上記の設定により追加されたカスタムの設定ファイルに、以下の設定を適宜追加してください。
Unity プロジェクトの設定
AndroidManifest の修正
カメラのレコーディングを実行するため、以下のパーミッションを追加してください。
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
実際に追加した [AndroidManifest.xml] の例
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.unity3d.player"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application>
<activity android:name="com.unity3d.player.UnityPlayerActivity"
android:theme="@style/UnityThemeSelector">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="unityplayer.UnityActivity" android:value="true" />
</activity>
</application>
</manifest>
Main gradle template の追加
ライブラリ側で AndroidX
のカメラ機能を利用するため以下の依存関係を追加してください。
def cameraVersion = "1.3.1"
implementation("androidx.camera:camera-lifecycle:$cameraVersion")
implementation("androidx.camera:camera-camera2:$cameraVersion")
implementation("androidx.camera:camera-view:$cameraVersion")
implementation("androidx.camera:camera-core:$cameraVersion")
実際に設定した [mainTemplate.gradle] の例
apply plugin: 'com.android.library'
**APPLY_PLUGINS**
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
def cameraVersion = "1.3.1"
implementation("androidx.camera:camera-lifecycle:$cameraVersion")
implementation("androidx.camera:camera-camera2:$cameraVersion")
implementation("androidx.camera:camera-view:$cameraVersion")
implementation("androidx.camera:camera-core:$cameraVersion")
**DEPS**}
android {
ndkPath "**NDKPATH**"
compileSdkVersion **APIVERSION**
buildToolsVersion '**BUILDTOOLS**'
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
defaultConfig {
minSdkVersion **MINSDKVERSION**
targetSdkVersion **TARGETSDKVERSION**
ndk {
abiFilters **ABIFILTERS**
}
versionCode **VERSIONCODE**
versionName '**VERSIONNAME**'
consumerProguardFiles 'proguard-unity.txt'**USER_PROGUARD**
}
lintOptions {
abortOnError false
}
aaptOptions {
noCompress = **BUILTIN_NOCOMPRESS** + unityStreamingAssets.tokenize(', ')
ignoreAssetsPattern = "!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~"
}**PACKAGING_OPTIONS**
}
**IL_CPP_BUILD_SETUP**
**SOURCE_BUILD_SETUP**
**EXTERNAL_SOURCES**
gradle properties の追加
上記の理由と同様に、gradleTemplate.properties
を追加する必要があります。生成されたテンプレートに以下を追加してください。
android.useAndroidX=true
実際に設定した [gradleTemplate.properties] の例
org.gradle.jvmargs=-Xmx**JVM_HEAP_SIZE**M
org.gradle.parallel=true
unityStreamingAssets=**STREAMING_ASSETS**
android.useAndroidX=true
**ADDITIONAL_PROPERTIES**
Unity C# からの呼び出し
Unity C# からは AndroidJavaClass
や AndroidJavaObject
を利用してアクセスします。
以下は、実装した CameraRecorder
のインスタンスを取得するコードです。インスタンス化する際に UnityPlayerActivity
を渡しています。
```cs:CamerRecorder インスタンスの作成
private void Initialize()
{
#if UNITY_ANDROID
AndroidJavaObject activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic<AndroidJavaObject>("currentActivity");
int cameraFacing = 0; // 0 = Front, 1 = Back
// namespace は実装したご自身のものに置き換えてください
_recorderObject = new AndroidJavaObject("tokyo.mesoncamerarecorder.CameraRecorder", activity, name, cameraFacing );
#endif
}
Unity から録画の開始・終了を呼び出す
インスタンスを無事作成できたら、あとは任意のタイミングで録画の開始・修了を呼び出します。
// 録画の開始
_recorderObject.Call("start");
// 録画の停止
_recorderObject.Call("stop");
録画終了時のコールバックの受け取り
保存完了後は UnityPlayer.SendMessage
を利用して Unity 側に通知します。コールバックを受け取るには、該当のメソッドを C# 側に実装しておきます。
public void CapturedVideo(string filePath)
{
Debug.Log(filePath);
}
以上で Unity 側の実装は完了です。
最後に
応募した Gemini コンペ向けのアプリでは定期的に動画撮影をして、その動画を AI に投げて日記を作成する、というような趣旨のアプリです。そのためにカメラ撮影機能が必要でした。
CameraX API を利用したカメラアプリの開発はとても簡単ですが、Unity を経由すると必要となる Activity
が用意できなかったりと少しハードルが高かったです。
もしかしたら Android 開発に精通しているとそこまで大変じゃないかもしれませんが、Android エンジニアではないので少し苦戦しました。
Unity アプリでカメラを使うケースは稀かと思いますが、なにかの参考になれば幸いです。
エンジニア絶賛募集中!
MESONではUnityエンジニアを絶賛募集中です! XRのプロジェクトに関わってみたい! 開発したい! という方はぜひご応募ください!
MESONのメンバーページからご応募いただくか、TwitterのDMなどでご連絡ください。
書いた人
比留間 和也(あだな:えど)
カヤック時代にWEBエンジニアとしてリーダーを務め、その後VRに出会いコロプラに転職。 コロプラでは仮想現実チームにてXRコンテンツ開発に携わる。 DAYDREAM向けゲーム「NYORO THE SNAKE & SEVEN ISLANDS」をリリース。その後、ARに惹かれてMESONに入社。 MESONではARエンジニアとして活躍中。
またプライベートでもAR/VRの開発をしており、インディー部門でTGSに出展など公私関わらずAR/VRコンテンツ制作に精を出す。プライベートな時間でも開発しているように、新しいことを学ぶことが趣味で、最近は英語を学んでいる。
MESON Works
MESONの制作実績一覧もあります。ご興味ある方はぜひ見てみてください。
Discussion