안드로이드에서 짧은 동영상 촬영 기능 구현

최근 프로젝트에서 짧은 동영상 업로드 기능을 추가해야 했습니다. 이 기능은 WeChat과 유사하게 촬영 시작 버튼을 클릭하고 최대 촬영 시간을 설정하는 방식으로 구현되었습니다. 아래에서는 해당 기능의 구현 방법을 공유합니다.

  1. 동영상 녹화 커스텀 뷰:
public class VideoRecorderView extends FrameLayout implements MediaRecorder.OnErrorListener {

    private SurfaceView surfaceView;
    private SurfaceHolder surfaceHolder;
    private ProgressBar progressBar;

    private MediaRecorder mediaRecorder;
    private Camera camera;
    private Timer timer;
    private OnRecordCompleteListener onRecordCompleteListener;

    private int videoWidth;
    private int videoHeight;
    private boolean openCameraInitially;
    private int maxRecordingTime;
    private int currentTimeCount;
    private File recordedFile;

    public VideoRecorderView(Context context) {
        super(context);
        init(context, null);
    }

    public VideoRecorderView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.VideoRecorderView);
        videoWidth = a.getInteger(R.styleable.VideoRecorderView_video_width, 320);
        videoHeight = a.getInteger(R.styleable.VideoRecorderView_video_height, 240);
        openCameraInitially = a.getBoolean(R.styleable.VideoRecorderView_open_camera_initially, true);
        maxRecordingTime = a.getInteger(R.styleable.VideoRecorderView_max_recording_time, 10);
        a.recycle();

        LayoutInflater.from(context).inflate(R.layout.video_recorder_view, this);
        surfaceView = findViewById(R.id.surface_view);
        progressBar = findViewById(R.id.progress_bar);
        progressBar.setMax(maxRecordingTime);

        surfaceHolder = surfaceView.getHolder();
        surfaceHolder.addCallback(new SurfaceHolderCallback());
        surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }

    private class SurfaceHolderCallback implements SurfaceHolder.Callback {

        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            if (!openCameraInitially) return;
            try {
                initializeCamera();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            if (!openCameraInitially) return;
            releaseCameraResources();
        }
    }

    private void initializeCamera() throws IOException {
        if (camera != null) {
            releaseCameraResources();
        }
        camera = Camera.open();
        setCameraParameters();
        camera.setDisplayOrientation(90);
        camera.setPreviewDisplay(surfaceHolder);
        camera.startPreview();
        camera.unlock();
    }

    private void setCameraParameters() {
        if (camera != null) {
            Camera.Parameters params = camera.getParameters();
            params.setRotation(90);
            camera.setParameters(params);
        }
    }

    private void releaseCameraResources() {
        if (camera != null) {
            camera.release();
            camera = null;
        }
    }

    private void createRecordDirectory() {
        File dir = new File(Environment.getExternalStorageDirectory(), "myapp/video/");
        if (!dir.exists()) dir.mkdirs();
        try {
            recordedFile = File.createTempFile("recording", ".mp4", dir);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void initializeRecorder() throws IOException {
        mediaRecorder = new MediaRecorder();
        mediaRecorder.reset();
        mediaRecorder.setCamera(camera);
        mediaRecorder.setOnErrorListener(this);
        mediaRecorder.setPreviewDisplay(surfaceHolder.getSurface());
        mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
        mediaRecorder.setVideoSize(videoWidth, videoHeight);
        mediaRecorder.setVideoEncodingBitRate(1 * 1024 * 1024);
        mediaRecorder.setOrientationHint(90);
        mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.MPEG_4_SP);
        mediaRecorder.setOutputFile(recordedFile.getAbsolutePath());
        mediaRecorder.prepare();
        mediaRecorder.start();
    }

    public void record(final OnRecordCompleteListener listener) {
        this.onRecordCompleteListener = listener;
        createRecordDirectory();
        try {
            if (!openCameraInitially) initializeCamera();
            initializeRecorder();
            currentTimeCount = 0;
            timer = new Timer();
            timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    currentTimeCount++;
                    progressBar.setProgress(currentTimeCount);
                    if (currentTimeCount == maxRecordingTime) {
                        stopRecording();
                        if (onRecordCompleteListener != null) onRecordCompleteListener.onComplete();
                    }
                }
            }, 0, 1000);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void stopRecording() {
        stopMediaRecorder();
        releaseRecorderResources();
        releaseCameraResources();
    }

    private void stopMediaRecorder() {
        if (mediaRecorder != null) {
            mediaRecorder.stop();
        }
        progressBar.setProgress(0);
        if (timer != null) timer.cancel();
    }

    private void releaseRecorderResources() {
        if (mediaRecorder != null) {
            mediaRecorder.release();
            mediaRecorder = null;
        }
    }

    public interface OnRecordCompleteListener {
        void onComplete();
    }

    @Override
    public void onError(MediaRecorder mr, int what, int extra) {
        if (mr != null) mr.reset();
    }
}
  1. 동영상 녹화 뷰 파일 video_recorder_view.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/black">

    <SurfaceView
        android:id="@+id/surface_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <ProgressBar
        android:id="@+id/progress_bar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="4dp"
        android:layout_gravity="bottom" />
</FrameLayout>

위 코드를 통해 기본적인 준비 작업이 완료되었습니다. 이제 본격적으로 동영상 녹화 기능을 구현할 차례입니다.

  1. 메인 액티비티 레이아웃 activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white"
    android:orientation="vertical">

    <com.example.videorecorddemo.VideoRecorderView
        android:id="@+id/video_recorder_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:layout_margin="5dp" />

    <Button
        android:id="@+id/record_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="촬영 시작"
        android:textColor="#ff6600" />
</LinearLayout>
  1. 메인 액티비티 MainActivity.java:
public class MainActivity extends AppCompatActivity {

    private VideoRecorderView recorderView;
    private Button recordButton;
    private boolean isCompleted = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        recorderView = findViewById(R.id.video_recorder_view);
        recordButton = findViewById(R.id.record_button);

        recordButton.setOnTouchListener((v, event) -> {
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                recordButton.setBackgroundResource(R.drawable.bg_record_pressed);
                recorderView.record(() -> {
                    if (!isCompleted && recorderView.getCurrentTimeCount() < 10) {
                        isCompleted = true;
                        handler.sendEmptyMessage(1);
                    }
                });
            } else if (event.getAction() == MotionEvent.ACTION_UP) {
                recordButton.setBackgroundResource(R.drawable.bg_record_normal);
                if (recorderView.getCurrentTimeCount() > 3) {
                    if (!isCompleted) {
                        isCompleted = true;
                        handler.sendEmptyMessage(1);
                    }
                } else {
                    isCompleted = false;
                    if (recorderView.getRecordedFile() != null)
                        recorderView.getRecordedFile().delete();
                    recorderView.stopRecording();
                    Toast.makeText(MainActivity.this, "동영상 길이가 너무 짧습니다.", Toast.LENGTH_SHORT).show();
                }
            }
            return true;
        });
    }

    @Override
    protected void onResume() {
        super.onResume();
        isCompleted = true;
        if (recorderView.getRecordedFile() != null)
            recorderView.getRecordedFile().delete();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        isCompleted = false;
        recorderView.stopRecording();
    }

    private Handler handler = new Handler(msg -> {
        if (isCompleted) finishActivity();
        return true;
    });

    private void finishActivity() {
        if (isCompleted) {
            recorderView.stopRecording();
            Intent intent = new Intent(this, SuccessActivity.class);
            Bundle bundle = new Bundle();
            bundle.putString("videoPath", recorderView.getRecordedFile().toString());
            intent.putExtras(bundle);
            startActivity(intent);
        }
        isCompleted = false;
    }
}
  1. 성공 화면 레이아웃 activity_success.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white"
    android:orientation="vertical">

    <TextView
        android:id="@+id/text_path"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="동영상 경로" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/play_button"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="재생" />

        <Button
            android:id="@+id/pause_button"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="일시정지" />

        <Button
            android:id="@+id/replay_button"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="다시 재생" />

        <Button
            android:id="@+id/duration_button"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="길이 확인" />
    </LinearLayout>

    <VideoView
        android:id="@+id/video_view"
        android:layout_width="match_parent"
        android:layout_height="500dp" />
</LinearLayout>
  1. 성공 화면 액티비티 SuccessActivity.java:
public class SuccessActivity extends AppCompatActivity implements View.OnClickListener {

    private TextView pathText;
    private Button playButton;
    private Button pauseButton;
    private Button replayButton;
    private Button durationButton;
    private VideoView videoView;
    private String filePath;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_success);
        Bundle extras = getIntent().getExtras();
        filePath = extras.getString("videoPath");
        initializeViews();
        setupViews();
    }

    private void initializeViews() {
        pathText = findViewById(R.id.text_path);
        playButton = findViewById(R.id.play_button);
        pauseButton = findViewById(R.id.pause_button);
        replayButton = findViewById(R.id.replay_button);
        durationButton = findViewById(R.id.duration_button);
        videoView = findViewById(R.id.video_view);
    }

    private void setupViews() {
        pathText.setText(filePath);
        playButton.setOnClickListener(this);
        pauseButton.setOnClickListener(this);
        replayButton.setOnClickListener(this);
        durationButton.setOnClickListener(this);
        videoView.setVideoPath(filePath);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.play_button:
                videoView.start();
                break;
            case R.id.pause_button:
                videoView.pause();
                break;
            case R.id.replay_button:
                videoView.resume();
                videoView.start();
                break;
            case R.id.duration_button:
                Toast.makeText(this, "동영상 길이: " + (videoView.getDuration() / 1000) + "초", Toast.LENGTH_SHORT).show();
                break;
        }
    }
}
  1. 권한 설정:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

태그: Android MediaRecorder Camera VideoView

7월 4일 20:10에 게시됨