MediaRecorder 개요

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

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

참고: 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)
    

    Java

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

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

MediaRecorder 인스턴스가 완료되면 release()를 호출하여 최대한 빨리 리소스를 해제합니다.

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

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

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

addTrack() 메서드를 사용하여 여러 트랙을 함께 믹스합니다.

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

메타데이터 추가

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

메타데이터 트랙을 추가할 때 트랙의 MIME 형식은 접두어 application/로 시작해야 합니다. 메타데이터 쓰기는 동영상 또는 오디오 데이터 쓰기와 동일하지만, 데이터를 MediaCodec에서 가져오지는 않습니다. 대신 앱은 연결된 타임스탬프가 있는 ByteBufferwriteSampleData() 메서드에 전달합니다. 타임스탬프는 동영상 및 오디오 트랙과 동일한 시간 베이스에 있어야 합니다.

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

샘플 코드

MediaRecorder 샘플은 MediaRecorder 및 Camera 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;
        }
    }
}

자세히 알아보기

이 페이지에서는 오디오와 동영상 녹음/녹화, 저장 및 재생과 관련된 주제를 다룹니다.