-
안드로이드 MediaPlayer mp4 비디오 재생 예제 (+VideoView)Android 2020. 7. 22. 00:09
안드로이드에서 비디오 파일을 재생하는 방법에 대해 남겨보려 한다. 초창기부터 비디오 플레이가 핵심 기능이었던 스마트폰의 역사에 걸맞게 안드로이드도 비디오 파일 재생에 있어서는 다양하고 편한 여러 가지 방법들을 제공한다. 거의 모든 안드로이드 폰에서 하드웨어 가속이 뒷받침 되기에 범용적으로 많이 쓰이는 미디어 파일들의 경우 전혀 무리없이 재생이 가능하다.
이번에는 여러 가지 방법들 중 가장 쉬운 VideoView를 활용한 방법과 한 단계 더 나아가서 MediaPlayer를 직접적으로 사용하는 방법에 대해서 남겨보려 한다. 미디어 컨트롤, 해상도 변경, 스트리밍 처리, 레이어 애니메이션 등 고급 주제는 배제하고 단순히 APK내부에 포함된 비디오 파일을 재생하는데 집중하겠다.
VideoView
비디오뷰는 안드로이드 위젯의 하나로 MediaPlayer를 이용한 비디오 파일 관련 구현을 내부에 포함하고 있다. 따라서 단순히 레이아웃에 위젯을 추가하고 비디오 파일을 지정하는 것만으로 쉽게 비디오 파일을 재생할 수 있다. 물론 세부적인 컨트롤을 하려면 뒤에 소개하는 것처럼 MediaPlayer를 직접 사용해야 하지만, 대부분의 경우는 VideoView로 원하는 목적을 달성할 수 있다.
가장 먼저 할 일은 레이아웃에 비디오뷰 위젯을 추가하는 것이다. 다음은 길이는 부모와 같고, 높이는 210dp가 되는 비디오뷰의 예제이다.
<VideoView android:id="@+id/screenVideoView" android:layout_width="match_parent" android:layout_height="210dp"> </VideoView>
다음으로 할 일은 비디오뷰에 재생하기 원하는 비디오 파일을 지정하는 것이다. 예를 들어 프로젝트의 raw 디렉토리에 포함된 sample.mp4를 재생하기 위해서는 다음과 같이 한다.
// 비디오뷰 가져오기 mVideoView = (VideoView) findViewById(R.id.screenVideoView); // sample.mp4 설정 Uri uri = Uri.parse("android.resource://" + getPackageName() + "/raw/sample"); mVideoView.setVideoURI(uri);
마지막으로 준비가 완료되면 비디오를 재생해 주기 위해 리스너를 등록한다.
// 리스너 등록 mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { // 준비 완료되면 비디오 재생 mp.start(); } });
이것으로 앱을 실행하면 sample.mp4 비디오가 플레이 되는 것을 볼 수 있다.
MediaPlayer
비디오뷰는 알고보면 MediaPlayer관련 구현을 내부에 감춘 래퍼에 불과한데, 이를 사용하지 않고 MediaPlayer를 직접 사용해서 비디오를 재생하는 것도 간단히 할 수 있다. 굳이 비디오뷰를 쓰지 않고 MediaPlayer를 사용하는 것은 비디오뷰로는 할 수 없는 세부적인 조작이 필요할 때가 대부분이다.
가장 먼저 할 일은 레이아웃에 MediaPlayer가 비디오를 출력할 타겟을 추가하는 것이다. 여러 MediaPlayer 예제들은 SurfaceView를 사용하는데, 이 경우 단순한 동작에는 큰 무리가 없지만 비디오 텍스쳐를 조작해서 다른 곳에 이용하거나 하는 등의 응용 동작이 어려워진다. 원래 SurfaceView의 목적은 유저가 인터렉션을 하며 그림을 그리는 등의 기능을 위한 것으로 스트리밍이나 비디오 파일의 재생은 TextureView를 사용하는게 더 적합하다.
한 가지 주의할 점은 TextureView를 사용할 경우 반드시 하드웨어 가속이 지원되는 경우에만 재생이 가능하다는 점이다. 하드웨어 가속이 지원되지 않는 비디오 포맷의 경우 화면에 아무 것도 나타나지 않으니 이런 경우에는 SurfaceView를 사용해야 한다. 사실 밑으로 더 내려가면 하드웨어 지원 코덱의 경우 이온 버퍼를 사용해 디코더->렌더 사이의 카피를 없앤 것이 원인인데, 고해상도 영상일수록 이 제로 카피가 성능에 큰 영향을 미친다. 여건이 허락하지 않는 상황을 제외한다면, 배터리 효율이나 성능 면에서 가능하면 하드웨어 코덱이 지원되는 영상 포맷을 사용하고 TextureView를 쓰는 것을 추천한다.
그래서 이번 예제에서는 MediaPlayer의 타겟으로 TextureView를 사용해 보려한다. 다음은 레이아웃에 추가된 높이 210dp의 TexureView 예제이다.
<TextureView android:id="@+id/screenTextureView" android:layout_width="match_parent" android:layout_height="210dp"> </TextureView>
다음으로 할 일은 만들어진 TextureView에 리스너를 추가하는 것이다. 일단 뷰가 준비된 이후 시점이 되어야 MediaPlayer를 준비할 수 있기 대문이다. TextureView.SurfaceTextureListener가 이 용도로 사용되는 리스너이다. 다음은 이를 구현한 MyTexureViewListener 예제이다.
protected class MyTexureViewListener implements TextureView.SurfaceTextureListener { Context mContext; String mVideoSource; public MyTexureViewListener(Context context, String source) { // 생성자는 편의상 컨텍스트와 비디오 파일의 이름을 받아서 저장해 둔다. mContext = context; mVideoSource = source; } @Override // 텍스쳐가 준비되면 불리는 메서드이다. 여기에서 초기화를 처리한다. public void onSurfaceTextureAvailable( SurfaceTexture surfaceTexture, int width, int height) { try { // 먼저 사용할 미디어 플레이어를 만든다. mPlayer = new MediaPlayer(); // 인자로 들어온 surfaceTexture를 기반으로 Surface를 하나 생성한다. Surface surface = new Surface(surfaceTexture); // 만들어진 Surface를 미디어 플레이어에 세팅한다. mPlayer.setSurface(surface); // 미디어 플레이어의 비디오 소스를 세팅한다. // 이 경우에는 raw 디렉토리의 sample.mp4다. 참고로 .mp4는 생략하고 ~/sample로 접근한다. Uri uri = Uri.parse( "android.resource://" + getPackageName() + "/raw/" + mVideoSource); mPlayer.setDataSource(mContext, uri); // 이제 미디어 플레이어를 준비시킨다. // 준비 함수는 prepare()와 prepareAsync()가 있는데 각각 동기, 비동기 버전이다. // 지금과 같은 로컬 파일의 경우 prepare()로 동기 처리를 해도 되지만, // 스트리밍을 할 때에는 반드시 prepareAsync()로 비동기 처리를 해야 한다. // 모든 경우에 그냥 비동기 처리를 하는 것이 좋은 습관이다. mPlayer.prepareAsync(); // 준비가 끝나는 지점을 알기 위해 리스너를 등록한다. // 비디오뷰에서 썼던 것과 같은 리스너이다. mPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { // 준비가 완료되면 비디오를 재생한다. mp.start(); } }); } catch (Exception e) { e.printStackTrace(); } } @Override public void onSurfaceTextureSizeChanged( SurfaceTexture surfaceTexture, int width, int height) { // 텍스쳐 사이즈가 변경되면 불린다. 여기에서는 처리하지 않는다. } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { // 텍스쳐가 파괴되면 불린다. 여기에서는 처리하지 않는다. return false; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { // 텍스쳐 업데이트가 일어나면 불린다. 미디어 플레이어가 계속 업데이트 중이므로 여기에서는 처리하지 않는다. } }
MyTexureViewListener는 onCreate 등 시점에서 다음과 같이 설정해 준다.
// 현재 액티비티를 컨텍스트로 주고, sample.mp4를 재생한다. // Raw 디렉토리 밑의 파일을 URI로 접근할 때는 확장자를 생략하므로 sample만 전달한다. mTextureView.setSurfaceTextureListener(new MyTexureViewListener(this, "sample"));
이제 앱을 실행하면 sample.mp4가 재생되는 것을 확인할 수 있다.
Example
상기에 설명한 VideoView와 MediaPlayer를 합친 통합 예제이다. 동일한 sample.mp4 영상을 각각 MediaPlayer와 VideoView를 통해 재생한다. 코드는 위에서 설명한 것과 대부분 동일하며 다른 점은 Play/Stop 버튼을 통해 영상을 조작할 수 있다는 점이다.
public class MainActivity extends AppCompatActivity { protected TextureView mTextureView = null; protected VideoView mVideoView = null; protected MediaPlayer mPlayer = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Get controls getControls(); // Prepare video for TextureView prepareTextureViewVideo(); // Prepare video for VideoView prepareVideoViewVideo(); } protected void getControls() { // Get buttons Button btn = (Button) findViewById(R.id.btnPlayTV); btn.setOnClickListener(new Button.OnClickListener() { @Override public void onClick(View v) { playTextureView(); } }); btn = (Button) findViewById(R.id.btnPlayVV); btn.setOnClickListener(new Button.OnClickListener() { @Override public void onClick(View v) { playVideoView(); } }); btn = (Button) findViewById(R.id.btnStopTV); btn.setOnClickListener(new Button.OnClickListener() { @Override public void onClick(View v) { stopTextureView(); } }); btn = (Button) findViewById(R.id.btnStopVV); btn.setOnClickListener(new Button.OnClickListener() { @Override public void onClick(View v) { stopVideoView(); } }); // Get views mTextureView = (TextureView) findViewById(R.id.screenTextureView); mVideoView = (VideoView) findViewById(R.id.screenVideoView); } ///////////////////////////////////////////////////////////////////////////////// // MediaPlayer related code from here ///////////////////////////////////////////////////////////////////////////////// protected class MyTexureViewListener implements TextureView.SurfaceTextureListener { Context mContext; String mVideoSource; public MyTexureViewListener(Context context, String source) { mContext = context; mVideoSource = source; } @Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { try { // Create MediaPlayer mPlayer = new MediaPlayer(); // Set the surface Surface surface = new Surface(surfaceTexture); mPlayer.setSurface(surface); // Set the video source Uri uri = Uri.parse("android.resource://" + getPackageName() + "/raw/" + mVideoSource); mPlayer.setDataSource(mContext, uri); // Prepare: In case of local file prepare() can be used, but for streaming, prepareAsync() is a must mPlayer.prepareAsync(); // Wait for the preparation mPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { // Play the video playTextureView(); } }); } catch (Exception e) { e.printStackTrace(); } } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) { } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { return false; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { } } protected void prepareTextureViewVideo() { // Set the listener to play "sample.mp4" in raw directory mTextureView.setSurfaceTextureListener(new MyTexureViewListener(this, "sample")); } protected void playTextureView() { // Play it mPlayer.start(); } protected void stopTextureView() { // Pause it. If stopped, mPlayer should be prepared again. mPlayer.pause(); } ///////////////////////////////////////////////////////////////////////////////// // VideoView related code from here ///////////////////////////////////////////////////////////////////////////////// protected void prepareVideoViewVideo() { // Set "sample.mp4" in raw directory Uri uri = Uri.parse("android.resource://" + getPackageName() + "/raw/sample"); mVideoView.setVideoURI(uri); // Set the listener mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { playVideoView(); } }); } protected void playVideoView() { mVideoView.start(); } protected void stopVideoView() { mVideoView.pause(); } }
참고로 예제에 사용된 레이아웃은 다음과 같다.
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="30dp" android:gravity="center_horizontal|center_vertical" android:text="[TextureView + MediaPlayer]"> </TextView> <TextureView android:id="@+id/screenTextureView" android:layout_width="match_parent" android:layout_height="210dp"> </TextureView> <LinearLayout android:layout_width="match_parent" android:layout_height="50dp" android:orientation="horizontal"> <Button android:id="@+id/btnPlayTV" android:layout_width="100dp" android:layout_height="40dp" android:text="Play"> </Button> <Button android:id="@+id/btnStopTV" android:layout_width="100dp" android:layout_height="40dp" android:text="Stop"> </Button> </LinearLayout> <TextView android:layout_width="match_parent" android:layout_height="30dp" android:gravity="center_horizontal|center_vertical" android:text="[VideoView]"> </TextView> <VideoView android:id="@+id/screenVideoView" android:layout_width="match_parent" android:layout_height="210dp"> </VideoView> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:id="@+id/btnPlayVV" android:layout_width="100dp" android:layout_height="40dp" android:text="Play"> </Button> <Button android:id="@+id/btnStopVV" android:layout_width="100dp" android:layout_height="40dp" android:text="Stop"> </Button> </LinearLayout> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
Fin.
반응형'Android' 카테고리의 다른 글
안드로이드 Glide와 CircleImageView로 원형 이미지 표시 (2) 2020.07.20 안드로이드 AsyncTask의 doInBackground 호출 안됨 (0) 2020.07.19 안드로이드 (Deprecated) AsyncTask 대체하기 (11) 2020.03.05 안드로이드 앱 완전히 종료하기 (0) 2019.09.19