はじめに (対象読者・この記事でわかること)

この記事は、Androidアプリに録画機能を実装しようとしているものの、なかなかうまく行かない開発者の皆様を対象にしています。特に、MediaRecorder APIの扱いに戸惑っている方や、パーミッション、ファイル保存の複雑さに頭を悩ませている方に読んでいただきたいです。

この記事を読むことで、Androidアプリで録画機能を実装するための基本的な流れと、MediaRecorder APIの正確な使い方を理解できます。さらに、録画機能の実装時によく直面する問題点(パーミッションエラー、ファイル保存失敗、録画が開始できないなど)とその具体的な解決策を学ぶことができます。筆者自身が直面した課題を元に、効果的な実装方法を共有します。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 * Javaプログラミングの基本的な知識 * Androidアプリ開発の基本的な知識(Activity, Fragment, UIコンポーネントの操作、ライフサイクルなど) * AndroidManifest.xml の基本的な理解

Androidにおける録画機能実装の概要と直面しがちな課題

Androidアプリで録画機能を実装する場合、主にデバイスのカメラを使用して映像をキャプチャし、MediaRecorder APIを使ってその映像と音声を記録、ファイルとして保存するのが一般的な流れです。カメラのプレビュー表示にはCameraXまたはCamera2 APIが使われることが多いですが、録画そのものの核となるのはMediaRecorderです。

しかし、この録画機能の実装は、一見シンプルに見えても多くの開発者がつまずきやすいポイントを含んでいます。主な課題は以下の通りです。

  1. パーミッション管理の複雑さ: 録画にはカメラ、マイク、外部ストレージへのアクセス権限が必要ですが、Androidのバージョンアップに伴いパーミッションの取得方法や要件が変化しています(特に実行時パーミッション)。
  2. MediaRecorderのライフサイクル管理: MediaRecorderprepare(), start(), stop(), release()といった一連のメソッドを特定の順序で呼び出す必要があり、この順序を間違えるとIllegalStateExceptionなどのエラーが発生します。
  3. デバイスの多様性と互換性: Androidデバイスは多種多様であり、カメラの特性(解像度、フレームレート、サポートされるエンコーダー/フォーマット)が異なります。これにより、特定のデバイスでは録画がうまくいかないといった問題が発生することがあります。
  4. ファイル保存の制限(スコープストレージ): Android 10 (API Level 29) 以降で導入されたスコープストレージにより、アプリが外部ストレージにファイルを自由に保存することが難しくなりました。MediaStore APIを用いた新しいファイル保存方法への対応が必須です。

これらの課題を乗り越え、安定した録画機能を実装するためには、各APIの仕様を正確に理解し、適切なエラーハンドリングを施す必要があります。

Androidで録画機能を実装する具体的な手順と解決策

ここからは、Androidアプリで録画機能を実装するための具体的な手順を、Javaコードを交えながら解説します。特に、前述の課題に対する解決策に焦点を当てていきます。

ステップ1: 必要なパーミッションの宣言と要求

録画機能には、カメラ、マイク、そして録画したファイルを保存するためのストレージへのアクセス権限が必要です。

1.1 AndroidManifest.xmlでの宣言

AndroidManifest.xmlに以下のパーミッションを追加します。

Xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.your_app_name"> <!-- カメラへのアクセス --> <uses-permission android:name="android.permission.CAMERA" /> <!-- マイクへのアクセス --> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <!-- 外部ストレージへの書き込み (Android 9以下の場合に必要) --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <!-- カメラ機能の必須宣言 (カメラがないデバイスにインストールされないように) --> <uses-feature android:name="android.hardware.camera" android:required="false" /> <uses-feature android:name="android.hardware.camera.autofocus" android:required="false" /> <application ... </application> </manifest>

注意: WRITE_EXTERNAL_STORAGEはAndroid 10 (API Level 29) 以降では不要または推奨されません。android:maxSdkVersion="28" を設定することで、Android 9 (API Level 28) までのデバイスにのみこのパーミッションが適用されます。Android 10以降はスコープストレージ (MediaStore API) を利用するため、明示的なストレージパーミッションは不要になります。

1.2 実行時パーミッションの要求

Android 6.0 (API Level 23) 以降では、危険なパーミッション(カメラ、マイク、ストレージなど)はアプリの実行時にユーザーの許可を得る必要があります。ActivityまたはFragmentで以下のコードを使用してパーミッションを要求します。

Java
import android.Manifest; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import java.util.ArrayList; import java.util.List; public class VideoRecordingActivity extends AppCompatActivity { private static final int REQUEST_PERMISSIONS_CODE = 100; private final String[] REQUIRED_PERMISSIONS = { Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO // Android 9以下の場合のみWRITE_EXTERNAL_STORAGEを追加 // Build.VERSION.SDK_INT <= Build.VERSION_CODES.P ? Manifest.permission.WRITE_EXTERNAL_STORAGE : "" }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_video_recording); checkAndRequestPermissions(); } private void checkAndRequestPermissions() { List<String> permissionsToRequest = new ArrayList<>(); for (String permission : REQUIRED_PERMISSIONS) { if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { permissionsToRequest.add(permission); } } // Android 9以下の場合はWRITE_EXTERNAL_STORAGEを追加 if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P && ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { permissionsToRequest.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); } if (!permissionsToRequest.isEmpty()) { ActivityCompat.requestPermissions(this, permissionsToRequest.toArray(new String[0]), REQUEST_PERMISSIONS_CODE); } else { // 全てのパーミッションが許可されている場合の処理 startCameraPreview(); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_PERMISSIONS_CODE) { boolean allPermissionsGranted = true; for (int result : grantResults) { if (result != PackageManager.PERMISSION_GRANTED) { allPermissionsGranted = false; break; } } if (allPermissionsGranted) { Toast.makeText(this, "全てのパーミッションが許可されました", Toast.LENGTH_SHORT).show(); startCameraPreview(); } else { Toast.makeText(this, "一部のパーミッションが拒否されました。アプリが正常に動作しない可能性があります。", Toast.LENGTH_LONG).show(); finish(); // アプリを終了するなど、適切な対応を行う } } } private void startCameraPreview() { // カメラプレビューの開始処理をここに記述 // 例: CameraXまたはCamera2 APIを使ったプレビュー表示 } }

ステップ2: カメラプレビューの表示とMediaRecorderの連携

録画機能のUIには、通常、カメラのプレビュー画面が必要です。ここではCameraXを前提に、SurfaceProviderを通じてMediaRecorderと連携する方法を示します。

Java
import android.media.MediaRecorder; import android.os.Environment; import android.view.Surface; import android.view.TextureView; // または SurfaceView import androidx.camera.core.CameraSelector; import androidx.camera.core.Preview; import androidx.camera.lifecycle.ProcessCameraProvider; import androidx.camera.view.PreviewView; import androidx.core.content.ContextCompat; import androidx.lifecycle.LifecycleOwner; import com.google.common.util.concurrent.ListenableFuture; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.concurrent.ExecutionException; public class VideoRecordingActivity extends AppCompatActivity { // ... (パーミッション関連のコードは省略) private PreviewView previewView; // CameraXのプレビュー表示用 private MediaRecorder mediaRecorder; private boolean isRecording = false; private File videoFile; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_video_recording); previewView = findViewById(R.id.preview_view); // ... (録画開始/停止ボタンなどのUI設定) if (allPermissionsGranted()) { // 仮のメソッド startCameraAndMediaRecorder(); } else { checkAndRequestPermissions(); } } private void startCameraAndMediaRecorder() { ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(this); cameraProviderFuture.addListener(() -> { try { ProcessCameraProvider cameraProvider = cameraProviderFuture.get(); Preview preview = new Preview.Builder().build(); // プレビュー表示先をPreviewViewに設定 preview.setSurfaceProvider(previewView.getSurfaceProvider()); CameraSelector cameraSelector = new CameraSelector.Builder() .requireLensFacing(CameraSelector.LENS_FACING_BACK) .build(); // カメラのバインド cameraProvider.unbindAll(); cameraProvider.bindToLifecycle((LifecycleOwner) this, cameraSelector, preview); } catch (ExecutionException | InterruptedException e) { // エラーハンドリング } }, ContextCompat.getMainExecutor(this)); } private void initMediaRecorder() throws IOException { // MediaRecorderインスタンスが既に存在する場合は解放 if (mediaRecorder != null) { mediaRecorder.release(); } mediaRecorder = new MediaRecorder(); // オーディオとビデオのソースを設定 (順序が重要) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); // CameraXとの連携にはSurfaceを指定 // 出力フォーマットとエンコーダーを設定 mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); // ビデオの解像度とフレームレートを設定 mediaRecorder.setVideoSize(1280, 720); // 例: HD 720p mediaRecorder.setVideoFrameRate(30); // 例: 30fps // 録画ファイルを設定 videoFile = getOutputMediaFile(); if (videoFile == null) { Toast.makeText(this, "録画ファイルの準備に失敗しました。", Toast.LENGTH_SHORT).show(); return; } mediaRecorder.setOutputFile(videoFile.getAbsolutePath()); // 出力方向を設定 (任意) // mediaRecorder.setOrientationHint(90); // 縦向きで録画する場合 // プレビュー表示先SurfaceをMediaRecorderに設定 (CameraXの場合は不要、後述のSurfaceで連携) try { mediaRecorder.prepare(); } catch (IOException e) { // prepare()失敗時のエラーハンドリング releaseMediaRecorder(); Toast.makeText(this, "MediaRecorderの準備に失敗しました: " + e.getMessage(), Toast.LENGTH_LONG).show(); throw e; } } // CameraXからMediaRecorderにSurfaceを提供する設定 (CaptureSessionのTargetとしてMediaRecorderのSurfaceを設定) // これはCameraXのVideoCapture UseCaseを使うことでより簡潔になりますが、 // ここではMediaRecorderのSurfaceVideoSourceを理解するために一般的な方法で説明します。 // CameraX VideoCapture UseCaseの例: // https://developer.android.com/training/camera/camerax/video-capture // またはCamera2 APIを使用する場合: // private void setupCaptureSession(CameraDevice cameraDevice, Surface previewSurface, Surface recorderSurface) { // try { // cameraDevice.createCaptureSession(Arrays.asList(previewSurface, recorderSurface), // new CameraCaptureSession.StateCallback() { /* ... */ }, handler); // } catch (CameraAccessException e) { /* ... */ } // } // MediaRecorderのsetVideoSource(MediaRecorder.VideoSource.SURFACE)とCameraX/Camera2の連携が肝となります。 // CameraXのVideoCapture UseCaseを使うと、この複雑な連携を抽象化できます。 private File getOutputMediaFile() { // Android 10 (API Level 29) 以上の場合: MediaStoreを使用 // 以下は簡易的な外部ストレージへの保存例 (非推奨または限定的な場合) File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_MOVIES), "YourAppName"); if (!mediaStorageDir.exists()) { if (!mediaStorageDir.mkdirs()) { Log.d("VideoRecord", "Failed to create directory"); return null; } } String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()); return new File(mediaStorageDir.getPath() + File.separator + "VID_" + timeStamp + ".mp4"); } // ... (録画開始/停止/解放のメソッド) }

ステップ3: 録画の開始・停止・解放

MediaRecorderのライフサイクルに沿って、録画の開始、停止、リソースの解放を行います。

Java
// VideoRecordingActivity クラス内 private void startRecording() { try { initMediaRecorder(); // MediaRecorderを初期化しprepare()まで行う mediaRecorder.start(); // 録画開始 isRecording = true; Toast.makeText(this, "録画を開始しました。", Toast.LENGTH_SHORT).show(); } catch (Exception e) { Toast.makeText(this, "録画開始に失敗しました: " + e.getMessage(), Toast.LENGTH_LONG).show(); releaseMediaRecorder(); // 失敗したら必ず解放 } } private void stopRecording() { if (isRecording) { try { mediaRecorder.stop(); // 録画停止 Toast.makeText(this, "録画を停止しました。ファイル: " + videoFile.getAbsolutePath(), Toast.LENGTH_LONG).show(); // TODO: MediaStoreに登録する処理 (Android 10+対応) } catch (RuntimeException e) { // stop()中にエラーが発生した場合 (例: 録画中にクラッシュ、ファイルが破損しているなど) Toast.makeText(this, "録画停止中にエラーが発生しました: " + e.getMessage(), Toast.LENGTH_LONG).show(); // ファイルが破損している可能性があるので削除を検討 if (videoFile != null && videoFile.exists()) { videoFile.delete(); } } finally { releaseMediaRecorder(); isRecording = false; } } } private void releaseMediaRecorder() { if (mediaRecorder != null) { mediaRecorder.reset(); // State reset mediaRecorder.release(); // リソース解放 mediaRecorder = null; } } @Override protected void onPause() { super.onPause(); if (isRecording) { stopRecording(); // アクティビティが一時停止したら録画を停止 } releaseMediaRecorder(); // 念のため解放 } @Override protected void onDestroy() { super.onDestroy(); releaseMediaRecorder(); // アクティビティが破棄されたら解放 }

ステップ4: ファイルの保存とパスの管理(Android 10以降のスコープストレージ対応)

Android 10 (API Level 29) 以降では、WRITE_EXTERNAL_STORAGEパーミッションが非推奨となり、スコープストレージが導入されました。アプリは自分専用のディレクトリか、MediaStore APIを通じて共有ストレージ(ギャラリーなど)にファイルを保存する必要があります。

MediaStore を使ったファイル保存 (推奨)

録画停止後、MediaStoreにファイルを登録することで、ギャラリーアプリなどで動画を閲覧できるようになります。

Java
import android.content.ContentResolver; import android.content.ContentValues; import android.net.Uri; import android.os.Build; import android.provider.MediaStore; import java.io.FileInputStream; import java.io.OutputStream; // ... (stopRecording() メソッド内、録画停止後) private void stopRecording() { if (isRecording) { try { mediaRecorder.stop(); // Android 10 (API Level 29) 以降 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && videoFile != null && videoFile.exists()) { saveVideoToMediaStore(videoFile); } else if (videoFile != null && videoFile.exists()) { // Android 9以下の場合、ファイルパスを直接通知 Toast.makeText(this, "録画停止しました。ファイル: " + videoFile.getAbsolutePath(), Toast.LENGTH_LONG).show(); } } catch (RuntimeException e) { // ... (エラーハンドリング) } finally { releaseMediaRecorder(); isRecording = false; if (videoFile != null && videoFile.exists() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // MediaStoreにコピーした後、一時ファイルを削除 videoFile.delete(); } } } } private void saveVideoToMediaStore(File file) { ContentResolver resolver = getContentResolver(); ContentValues contentValues = new ContentValues(); contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, file.getName()); contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4"); contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_MOVIES + File.separator + "YourAppName"); Uri collectionUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; Uri newVideoUri = resolver.insert(collectionUri, contentValues); if (newVideoUri != null) { try (OutputStream os = resolver.openOutputStream(newVideoUri); FileInputStream fis = new FileInputStream(file)) { if (os != null) { byte[] buffer = new byte[1024]; int len; while ((len = fis.read(buffer)) > 0) { os.write(buffer, 0, len); } Toast.makeText(this, "動画をギャラリーに保存しました!", Toast.LENGTH_SHORT).show(); } } catch (IOException e) { Toast.makeText(this, "動画の保存に失敗しました: " + e.getMessage(), Toast.LENGTH_LONG).show(); if (newVideoUri != null) { resolver.delete(newVideoUri, null, null); // 失敗したら作成したエントリを削除 } } } else { Toast.makeText(this, "動画のURI取得に失敗しました。", Toast.LENGTH_LONG).show(); } }

ハマった点やエラー解決

1. MediaRecorder.prepare()IllegalStateExceptionが発生する

  • 原因:
    • setAudioSource(), setVideoSource(), setOutputFormat(), setVideoEncoder(), setAudioEncoder(), setOutputFile()など、必須の設定が漏れているか、設定順序が間違っている。
    • カメラやマイクが既に他のアプリで使用されている。
    • 必要なパーミッションが許可されていない。
    • setPreviewDisplay()(古いAPIの場合)に無効なSurfaceが渡されている。
  • 解決策:
    • MediaRecorderの初期化シーケンス(ソース設定 -> フォーマット設定 -> エンコーダー設定 -> 出力ファイル設定 -> prepare())を厳密に守る。
    • prepare()を呼び出す前にmediaRecorder.reset()を呼び出して状態をクリアする(リサイクル時)。
    • パーミッションが全て許可されているか再確認する。
    • 他のカメラアプリが起動していないか確認する。

2. 録画されたファイルが破損している、または再生できない

  • 原因:
    • setOutputFormat()setVideoEncoder()setAudioEncoder()の組み合わせがデバイスでサポートされていない。
    • 録画中にアプリがクラッシュしたり、stop()が正常に呼び出されなかった。
    • release()が呼び出されず、リソースが適切に解放されていない。
  • 解決策:
    • 一般的なフォーマット (MPEG_4) とエンコーダー (H264, AAC) の組み合わせを使用する。
    • try-catch-finallyブロックでstop()release()を確実に呼び出す。
    • RuntimeException(特にstop()時)を捕捉し、必要であれば破損したファイルを削除するロジックを追加する。

3. 録画中にアプリがクラッシュする、メモリリークが発生する

  • 原因:
    • MediaRecorderCamera関連のリソースがonDestroy()onPause()で適切に解放されていない。
    • 高解像度・高フレームレートでの長時間録画により、メモリやCPU負荷が高すぎる。
  • 解決策:
    • アクティビティやフラグメントのライフサイクルメソッド (onPause(), onDestroy()) で、MediaRecorderおよびカメラ関連のリソースを必ずrelease()する。
    • MediaRecorderインスタンスの再利用時は、reset()の後に再設定を行う。
    • デバイスの性能に合わせて、適切な解像度やフレームレートを設定する。

4. Android 10以降でファイルが外部ストレージに保存できない

  • 原因: スコープストレージの導入により、アプリの外部ストレージへの直接書き込みが大幅に制限されたため。
  • 解決策: MediaStore APIを使用し、ContentResolverを通じて動画ファイルを保存する。上記「ステップ4」のコード例を参照してください。一時的に録画ファイルを内部ストレージに保存し、その後MediaStore経由で共有ストレージにコピーする方法が一般的です。

まとめ

本記事では、Androidアプリで録画機能を実装する際の一般的な課題と、MediaRecorder APIを用いた具体的な解決策を解説しました。

  • パーミッションの適切な管理: AndroidManifest.xmlでの宣言と、実行時パーミッションの要求・ハンドリングが必須です。
  • MediaRecorderの正確なライフサイクル: initMediaRecorder()での各種設定、prepare(), start(), stop(), release()の順序とエラーハンドリングが重要です。
  • Android 10以降のファイル保存: スコープストレージに対応するため、MediaStore APIを利用したファイル保存が推奨されます。

この記事を通して、Androidアプリで録画機能を実装する際の障壁を乗り越え、より安定したアプリ開発に繋がるヒントを得られたことと思います。

今後は、CameraXVideoCapture UseCaseを使ったよりモダンで簡潔な録画機能の実装方法や、録画後の動画編集機能の統合についても記事にする予定です。

参考資料