최근 프로젝트에서 짧은 동영상 업로드 기능을 추가해야 했습니다. 이 기능은 WeChat과 유사하게 촬영 시작 버튼을 클릭하고 최대 촬영 시간을 설정하는 방식으로 구현되었습니다. 아래에서는 해당 기능의 구현 방법을 공유합니다.
- 동영상 녹화 커스텀 뷰:
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();
}
}
- 동영상 녹화 뷰 파일 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>
위 코드를 통해 기본적인 준비 작업이 완료되었습니다. 이제 본격적으로 동영상 녹화 기능을 구현할 차례입니다.
- 메인 액티비티 레이아웃 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>
- 메인 액티비티 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;
}
}
- 성공 화면 레이아웃 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>
- 성공 화면 액티비티 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;
}
}
}
- 권한 설정:
<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" />