MediaRecorder 개요

Android 멀티미디어 프레임워크에는 다양한 일반 오디오 및 동영상 포맷을 캡처하고 인코딩하는 지원 기능이 포함되어 있습니다. 기기 하드웨어에서 지원되는 경우 MediaRecorder API를 사용할 수 있습니다.

이 문서에서는 MediaRecorder를 사용하여 기기 마이크에서 오디오를 캡처하는 애플리케이션을 작성하고 오디오를 저장 및 재생(MediaPlayer 사용)하는 방법을 보여줍니다. 동영상을 녹음하려면 와 함께 기기의 카메라를 사용해야 합니다. 이에 대해서는 카메라 가이드에 설명되어 있습니다.

참고: Android Emulator는 오디오를 녹음할 수 없습니다. 녹음할 수 있는 실제 기기에서 코드를 테스트해야 합니다.

오디오 녹음 권한 요청

녹음하려면 앱에서 사용자에게 기기의 오디오 입력에 액세스할 것임을 알리는 메시지를 표시해야 합니다. 앱의 매니페스트 파일에 다음 권한 태그를 포함해야 합니다.

    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    

RECORD_AUDIO는 사용자의 개인정보 보호에 위험을 초래할 수 있기 때문에 '위험한' 권한으로 간주됩니다. Android 6.0(API 레벨 23)부터 위험한 권한을 사용하는 앱은 런타임에 사용자에게 승인을 요청해야 합니다. 사용자가 권한을 부여하면 앱은 이를 기억하고 다시 묻지 않습니다. 아래의 샘플 코드는 ActivityCompat.requestPermissions()를 사용하여 이 동작을 구현하는 방법을 보여줍니다.

MediaRecorder 만들기 및 실행

다음 호출을 사용하여 MediaRecorder의 새 인스턴스를 초기화합니다.

  • setAudioSource()를 사용하여 오디오 소스를 설정합니다. 대개 MIC가 사용됩니다.

    참고: 대부분의 오디오 소스(DEFAULT 포함)는 오디오 신호에 처리를 적용합니다. 원본 오디오를 녹음하려면 UNPROCESSED를 선택합니다. 일부 기기는 처리되지 않은 입력을 지원하지 않습니다. 먼저 AudioManager.getProperty(AudioManager.PROPERTY_SUPPORT_AUDIO_SOURCE_UNPROCESSED)를 호출하여 사용 가능한지 확인합니다. 사용 가능하지 않으면 AGC 또는 노이즈 제거 기능이 없는 VOICE_RECOGNITION을 사용해 봅니다. UNPROCESSED가 지원되지 않는 경우에도 이 속성을 오디오 소스로 사용할 수 있지만, 이 경우 신호의 처리 여부가 보장되지 않습니다.

  • setOutputFormat()을 사용하여 출력 파일 포맷을 설정합니다. Android 8.0(API 레벨 26)부터 MediaRecorder는 스트리밍에 유용한 MPEG2_TS 포맷을 지원합니다.

    Kotlin

        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_2_TS)
        

    자바

        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_2_TS);
        
  • setOutputFile()을 사용하여 출력 파일 이름을 설정합니다. 실제 파일을 나타내는 파일 설명자를 지정해야 합니다.
  • setAudioEncoder()를 사용하여 오디오 인코더를 설정합니다.
  • prepare()를 호출하여 초기화를 완료합니다.

start()stop()을 각각 호출하여 녹음기를 시작 및 중지합니다.

작업이 완료되면 MediaRecorderrelease()를 호출하여 가능한 한 빨리 리소스를 확보합니다.

참고: Android 9(API 레벨 28) 이상을 실행하는 기기에서 백그라운드로 실행되는 앱은 마이크에 액세스할 수 없습니다. 따라서 앱은 포그라운드에 있거나 포그라운드 서비스MediaRecorder의 인스턴스가 포함된 경우에만 오디오를 녹음합니다.

MediaMuxer를 사용하여 여러 채널 녹음하기

Android 8.0(API 레벨 26)부터 MediaMuxer를 사용하여 동시에 여러 개의 오디오 및 동영상 스트림을 녹음할 수 있습니다. 이전 버전의 Android에서는 한 번에 하나의 오디오 트랙 및/또는 하나의 동영상 트랙만 녹음할 수 있습니다.

addTrack() 메서드를 사용하여 여러 개의 트랙을 함께 혼합할 수 있습니다.

또한 각 프레임의 맞춤 정보가 있는 메타데이터 트랙을 하나 이상 추가할 수 있습니다. 단, MP4 컨테이너에만 추가할 수 있습니다. 앱은 메타데이터의 포맷과 콘텐츠를 정의합니다.

메타데이터 추가

메타데이터는 오프라인 처리에 유용할 수 있습니다. 예를 들어 자이로 센서에서 캡처한 데이터를 사용하여 동영상 안정화 작업을 할 수 있습니다.

메타데이터 트랙을 추가할 때 트랙의 MIME 포맷은 접두사 application/로 시작해야 합니다. 메타데이터를 작성하는 것은 데이터의 출처가 MediaCodec이 아니라는 점을 제외하면 동영상 데이터 또는 오디오 데이터를 작성하는 것과 동일합니다. 대신 앱은 ByteBuffer를 연결된 타임스탬프와 함께 writeSampleData() 메서드에 전달합니다. 타임스탬프는 동영상 및 오디오 트랙과 동일한 시간 베이스에 있어야 합니다.

생성된 MP4 파일은 ISO BMFF 사양의 12.3.3.2항에 정의된 TextMetaDataSampleEntry를 사용하여 메타데이터의 MIME 포맷을 알립니다. MediaExtractor를 사용하여 메타데이터 트랙을 포함하는 파일을 추출할 경우 메타데이터의 MIME 포맷은 MediaFormat의 인스턴스로 표시됩니다.

샘플 코드

MediaRecorder 샘플은 MediaRecorder 및 카메라 API를 사용하여 동영상을 녹음하는 방법을 보여줍니다.

아래의 예시 작업은 MediaRecorder를 사용하여 오디오 파일을 녹음하는 방법을 보여줍니다. 또한 MediaPlayer를 사용하여 오디오를 다시 재생합니다.

Kotlin

    package com.android.audiorecordtest

    import android.Manifest
    import android.content.Context
    import android.content.pm.PackageManager
    import android.media.MediaPlayer
    import android.media.MediaRecorder
    import android.os.Bundle
    import android.support.v4.app.ActivityCompat
    import android.support.v7.app.AppCompatActivity
    import android.util.Log
    import android.view.View.OnClickListener
    import android.view.ViewGroup
    import android.widget.Button
    import android.widget.LinearLayout
    import java.io.IOException

    private const val LOG_TAG = "AudioRecordTest"
    private const val REQUEST_RECORD_AUDIO_PERMISSION = 200

    class AudioRecordTest : AppCompatActivity() {

        private var fileName: String = ""

        private var recordButton: RecordButton? = null
        private var recorder: MediaRecorder? = null

        private var playButton: PlayButton? = null
        private var player: MediaPlayer? = null

        // Requesting permission to RECORD_AUDIO
        private var permissionToRecordAccepted = false
        private var permissions: Array<String> = arrayOf(Manifest.permission.RECORD_AUDIO)

        override fun onRequestPermissionsResult(
                requestCode: Int,
                permissions: Array<String>,
                grantResults: IntArray
        ) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults)
            permissionToRecordAccepted = if (requestCode == REQUEST_RECORD_AUDIO_PERMISSION) {
                grantResults[0] == PackageManager.PERMISSION_GRANTED
            } else {
                false
            }
            if (!permissionToRecordAccepted) finish()
        }

        private fun onRecord(start: Boolean) = if (start) {
            startRecording()
        } else {
            stopRecording()
        }

        private fun onPlay(start: Boolean) = if (start) {
            startPlaying()
        } else {
            stopPlaying()
        }

        private fun startPlaying() {
            player = MediaPlayer().apply {
                try {
                    setDataSource(fileName)
                    prepare()
                    start()
                } catch (e: IOException) {
                    Log.e(LOG_TAG, "prepare() failed")
                }
            }
        }

        private fun stopPlaying() {
            player?.release()
            player = null
        }

        private fun startRecording() {
            recorder = MediaRecorder().apply {
                setAudioSource(MediaRecorder.AudioSource.MIC)
                setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
                setOutputFile(fileName)
                setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)

                try {
                    prepare()
                } catch (e: IOException) {
                    Log.e(LOG_TAG, "prepare() failed")
                }

                start()
            }
        }

        private fun stopRecording() {
            recorder?.apply {
                stop()
                release()
            }
            recorder = null
        }

        internal inner class RecordButton(ctx: Context) : Button(ctx) {

            var mStartRecording = true

            var clicker: OnClickListener = OnClickListener {
                onRecord(mStartRecording)
                text = when (mStartRecording) {
                    true -> "Stop recording"
                    false -> "Start recording"
                }
                mStartRecording = !mStartRecording
            }

            init {
                text = "Start recording"
                setOnClickListener(clicker)
            }
        }

        internal inner class PlayButton(ctx: Context) : Button(ctx) {
            var mStartPlaying = true
            var clicker: OnClickListener = OnClickListener {
                onPlay(mStartPlaying)
                text = when (mStartPlaying) {
                    true -> "Stop playing"
                    false -> "Start playing"
                }
                mStartPlaying = !mStartPlaying
            }

            init {
                text = "Start playing"
                setOnClickListener(clicker)
            }
        }

        override fun onCreate(icicle: Bundle?) {
            super.onCreate(icicle)

            // Record to the external cache directory for visibility
            fileName = "${externalCacheDir.absolutePath}/audiorecordtest.3gp"

            ActivityCompat.requestPermissions(this, permissions, REQUEST_RECORD_AUDIO_PERMISSION)

            recordButton = RecordButton(this)
            playButton = PlayButton(this)
            val ll = LinearLayout(this).apply {
                addView(recordButton,
                        LinearLayout.LayoutParams(
                                ViewGroup.LayoutParams.WRAP_CONTENT,
                                ViewGroup.LayoutParams.WRAP_CONTENT,
                                0f))
                addView(playButton,
                        LinearLayout.LayoutParams(
                                ViewGroup.LayoutParams.WRAP_CONTENT,
                                ViewGroup.LayoutParams.WRAP_CONTENT,
                                0f))
            }
            setContentView(ll)
        }

        override fun onStop() {
            super.onStop()
            recorder?.release()
            recorder = null
            player?.release()
            player = null
        }
    }
    

자바

    package com.android.audiorecordtest;

    import android.Manifest;
    import android.content.Context;
    import android.content.pm.PackageManager;
    import android.media.MediaPlayer;
    import android.media.MediaRecorder;
    import android.os.Bundle;
    import android.support.annotation.NonNull;
    import android.support.v4.app.ActivityCompat;
    import android.support.v7.app.AppCompatActivity;
    import android.util.Log;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.Button;
    import android.widget.LinearLayout;

    import java.io.IOException;

    public class AudioRecordTest extends AppCompatActivity {

        private static final String LOG_TAG = "AudioRecordTest";
        private static final int REQUEST_RECORD_AUDIO_PERMISSION = 200;
        private static String fileName = null;

        private RecordButton recordButton = null;
        private MediaRecorder recorder = null;

        private PlayButton   playButton = null;
        private MediaPlayer   player = null;

        // Requesting permission to RECORD_AUDIO
        private boolean permissionToRecordAccepted = false;
        private String [] permissions = {Manifest.permission.RECORD_AUDIO};

        @Override
        public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
            switch (requestCode){
                case REQUEST_RECORD_AUDIO_PERMISSION:
                    permissionToRecordAccepted  = grantResults[0] == PackageManager.PERMISSION_GRANTED;
                    break;
            }
            if (!permissionToRecordAccepted ) finish();

        }

        private void onRecord(boolean start) {
            if (start) {
                startRecording();
            } else {
                stopRecording();
            }
        }

        private void onPlay(boolean start) {
            if (start) {
                startPlaying();
            } else {
                stopPlaying();
            }
        }

        private void startPlaying() {
            player = new MediaPlayer();
            try {
                player.setDataSource(fileName);
                player.prepare();
                player.start();
            } catch (IOException e) {
                Log.e(LOG_TAG, "prepare() failed");
            }
        }

        private void stopPlaying() {
            player.release();
            player = null;
        }

        private void startRecording() {
            recorder = new MediaRecorder();
            recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
            recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
            recorder.setOutputFile(fileName);
            recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);

            try {
                recorder.prepare();
            } catch (IOException e) {
                Log.e(LOG_TAG, "prepare() failed");
            }

            recorder.start();
        }

        private void stopRecording() {
            recorder.stop();
            recorder.release();
            recorder = null;
        }

        class RecordButton extends Button {
            boolean mStartRecording = true;

            OnClickListener clicker = new OnClickListener() {
                public void onClick(View v) {
                    onRecord(mStartRecording);
                    if (mStartRecording) {
                        setText("Stop recording");
                    } else {
                        setText("Start recording");
                    }
                    mStartRecording = !mStartRecording;
                }
            };

            public RecordButton(Context ctx) {
                super(ctx);
                setText("Start recording");
                setOnClickListener(clicker);
            }
        }

        class PlayButton extends Button {
            boolean mStartPlaying = true;

            OnClickListener clicker = new OnClickListener() {
                public void onClick(View v) {
                    onPlay(mStartPlaying);
                    if (mStartPlaying) {
                        setText("Stop playing");
                    } else {
                        setText("Start playing");
                    }
                    mStartPlaying = !mStartPlaying;
                }
            };

            public PlayButton(Context ctx) {
                super(ctx);
                setText("Start playing");
                setOnClickListener(clicker);
            }
        }

        @Override
        public void onCreate(Bundle icicle) {
            super.onCreate(icicle);

            // Record to the external cache directory for visibility
            fileName = getExternalCacheDir().getAbsolutePath();
            fileName += "/audiorecordtest.3gp";

            ActivityCompat.requestPermissions(this, permissions, REQUEST_RECORD_AUDIO_PERMISSION);

            LinearLayout ll = new LinearLayout(this);
            recordButton = new RecordButton(this);
            ll.addView(recordButton,
                    new LinearLayout.LayoutParams(
                            ViewGroup.LayoutParams.WRAP_CONTENT,
                            ViewGroup.LayoutParams.WRAP_CONTENT,
                            0));
            playButton = new PlayButton(this);
            ll.addView(playButton,
                    new LinearLayout.LayoutParams(
                            ViewGroup.LayoutParams.WRAP_CONTENT,
                            ViewGroup.LayoutParams.WRAP_CONTENT,
                            0));
            setContentView(ll);
        }

        @Override
        public void onStop() {
            super.onStop();
            if (recorder != null) {
                recorder.release();
                recorder = null;
            }

            if (player != null) {
                player.release();
                player = null;
            }
        }
    }
    

자세히 알아보기

다음 페이지에는 오디오 및 동영상의 녹음, 저장 및 재생과 관련된 주제가 설명되어 있습니다.