안드로이드에서 비디오 플레이 하기
조대협 (http://bcho.tistory.com)
동영상 서비스를 하다보니, 실제로 비디오 플레이어를 어떻게 구현하는 지가 궁금해서 찾아봤는데, 생각보다 안드로이드의 비디오 플레이어 구현은 간단해 보인다.
내부적으로 MediaPlayer라는 컴포넌트를 사용하는데, 이 MediaPlayer는 조금 더 미세한 컨트롤이 가능하고 이를 한단계 더 쌓아 놓은 VideoView라는 것이 사용방법이 쉽다. 또한 구버전의 안드로이드에서는 MediaPlayer 기능이 잘 작동하지 않는 부분이 있기 때문에, VideoView를 사용하는 것이 조금 더 안정적일 수 있다.
내장된 플레이어로 지원되는 프로토콜은 다음과 같다.
• RTSP
• HTTP/HTTPS Progressive Streaming
• HTTP/HTTPS Live Streaming (HLS)
◦ MPEG-2 TS
◦ Protocol version 3 (Android 4.0 & above)
◦ Protocol version 2 (Android 3.X)
◦ 3.0 이하 버전은 지원 안됨
MediaPlayer는 실시간 방송을 위한 RTMP나 HTTP 기반의 DASH등 다양한 프로토콜을 지원하지 않고, 기능상에 제약이 있는데, 이러한 기능상의 제약을 없애고 플레이어를 확장하려면, MediaCodec과 MediaExtractor 클래스를 이용하여 직접 플레이어를 만들어야 하는데, 번거롭고 다른 대안으로는 조금 더 다양한 기능을 제공하는 오픈 소스 플레이어를 활용하는 방법이 있다.
https://developer.android.com/guide/topics/media/exoplayer.html
를 보면 ExoPlayer를 소개하고 있는데, 안드로이드 공식 개발자 페이지에서 소개하는 것을 보면 신뢰성이 꽤 높은 듯 하다.
배경 설명은 이정도로 하고, 여기서는 간단하게 VieoView를 이용한 간단한 플레이어를 만들어 보도록 한다.
다음과 같은 기능을 구현한다.
• URL 입력 (Progressive download 또는 HLS)
• 재생
• 일시 멈춤
• 네비게이션 바 출력
• 전체 시간 출력
완성된 앱의 UI는 다음과 같다.
가장 기본적인 기능인데, 앞으로 가기나, 뒤로 가기,음소거,음켜기 등도 구현이 어렵지 않으니 쉽게 추 가 할 수 있다.
참고
미디어 플레이어 예제 http://www.tutorialspoint.com/android/android_mediaplayer.htm
전체화면 보기 예제
http://javaexpert.tistory.com/365
레이아웃 코드는 다음과 같다.
다음은, MainActivity.java 코드이다.
코드를 하나하나 뜯어 보면, 먼저 OnCreate에서 객체가 생성될때, VideoView 객체와 비디오 플레이 진행상황을 표시할 SeekBar에 대한 레퍼런스를 저장해놓는다.
final static String SAMPLE_VIDEO_URL = "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4";
VideoView videoView;
SeekBar seekBar;
Handler updateHandler = new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
EditText tvURL = (EditText)findViewById(R.id.etVieoURL);
tvURL.setText(SAMPLE_VIDEO_URL);
videoView = (VideoView)findViewById(R.id.videoView);
// MediaController mc = new MediaController(this);
// videoView.setMediaController(mc);
seekBar = (SeekBar)findViewById(R.id.seekBar);
}
다음 “LOAD” 버튼을 누르면 지정된 URL에서 MP4 파일을 읽어와서 플레이를 시작하도록 한다.
public void loadVideo(View view) {
//Sample video URL : http://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_2mb.mp4
EditText tvURL = (EditText) findViewById(R.id.etVieoURL);
String url = tvURL.getText().toString();
Toast.makeText(getApplicationContext(), "Loading Video. Plz wait", Toast.LENGTH_LONG).show();
videoView.setVideoURI(Uri.parse(url));
videoView.requestFocus();
// 토스트 다이얼로그를 이용하여 버퍼링중임을 알린다.
videoView.setOnInfoListener(new MediaPlayer.OnInfoListener() {
@Override
public boolean onInfo(MediaPlayer mp, int what, int extra) {
switch(what){
case MediaPlayer.MEDIA_INFO_BUFFERING_START:
// Progress Diaglog 출력
Toast.makeText(getApplicationContext(), "Buffering", Toast.LENGTH_LONG).show();
break;
case MediaPlayer.MEDIA_INFO_BUFFERING_END:
// Progress Dialog 삭제
Toast.makeText(getApplicationContext(), "Buffering finished.\nResume playing", Toast.LENGTH_LONG).show();
videoView.start();
break;
}
return false;
}
}
);
// 플레이 준비가 되면, seekBar와 PlayTime을 세팅하고 플레이를 한다.
videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
videoView.start();
long finalTime = videoView.getDuration();
TextView tvTotalTime = (TextView) findViewById(R.id.tvTotalTime);
tvTotalTime.setText(String.format("%d:%d",
TimeUnit.MILLISECONDS.toMinutes((long) finalTime),
TimeUnit.MILLISECONDS.toSeconds((long) finalTime) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes((long) finalTime)))
);
seekBar.setMax((int) finalTime);
seekBar.setProgress(0);
updateHandler.postDelayed(updateVideoTime, 100);
//Toast Box
Toast.makeText(getApplicationContext(), "Playing Video", Toast.LENGTH_SHORT).show();
}
});
}
하나씩 뜯어보면, 비디오 플레이는 viewView.SetVideoURI(URI)를 지정하면 비디오가 플레이가 시작되고, videoView.requestFocus()를 하면, 해당 view에 포커스를 한다.
다음으로 videoView.start()를 하면 플레이가 되는데, 중간에 몇가지 추가 코드가 들어가 있다.
먼저 이 예제는 로컬에 있는 파일을 플레이 하는 것이 아니라 Remote에 있는 MP4파일을 http로 다운 받으면서 플레이하는 Progressive download 형태의 플레이 방식이기 때문에 네트워크 상태에 따라서 로딩 시간이 걸릴 수 있다. 다음 videoView.setOnInfoListner는 VideoView의 상태가 변경되었을때 호출되는 콜백함수로, 버퍼링이 시작될때와 버퍼링이 끝날때의 이벤트를 잡아서 토스트 다이얼로그를 띄워주는 부분이다. 아래 구현에서는 토스트 다이얼로그를 사용했지만 Progress Dialog등을 이용하여 버퍼링 중임을 알려줄 수 도 있다.
버퍼링이 시작될때는 MediaPlayer.MEDIA_INFO_BUFFERING_START라는 이벤트를 보내고, 버퍼링이 끝날때는 MediaPlayer.MEDIA_INFO_BUFFERING_END라는 이벤트를 보내기 때문에, 이 시점에 맞춰서 버퍼링중임을 알리는 메세지를 보이거나 감추면 된다.
// 토스트 다이얼로그를 이용하여 버퍼링중임을 알린다.
videoView.setOnInfoListener(new MediaPlayer.OnInfoListener() {
@Override
public boolean onInfo(MediaPlayer mp, int what, int extra) {
switch(what){
case MediaPlayer.MEDIA_INFO_BUFFERING_START:
// Progress Diaglog 출력
Toast.makeText(getApplicationContext(), "Buffering", Toast.LENGTH_LONG).show();
break;
case MediaPlayer.MEDIA_INFO_BUFFERING_END:
// Progress Dialog 삭제
Toast.makeText(getApplicationContext(), "Buffering finished.\nResume playing", Toast.LENGTH_LONG).show();
videoView.start();
break;
}
return false;
}
}
);
다음 코드 부분은 버퍼링이 다 끝나고 처음 플레이 준비가 되었을 때 전체 플레이 시간을 세팅하고 SeekBar의 전체 길이등을 세팅하는 초기 작업을 하기 위한 콜백 함수이다. setOnPreparedListner에서, MediaPlayer.OnPreparedListner를 콜백함수로 다음과 같이 정의한다.
// 플레이 준비가 되면, seekBar와 PlayTime을 세팅하고 플레이를 한다.
videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
videoView.start();
long finalTime = videoView.getDuration();
TextView tvTotalTime = (TextView) findViewById(R.id.tvTotalTime);
tvTotalTime.setText(String.format("%d:%d",
TimeUnit.MILLISECONDS.toMinutes((long) finalTime),
TimeUnit.MILLISECONDS.toSeconds((long) finalTime) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes((long) finalTime)))
);
seekBar.setMax((int) finalTime);
seekBar.setProgress(0);
updateHandler.postDelayed(updateVideoTime, 100);
//Toast Box
Toast.makeText(getApplicationContext(), "Playing Video", Toast.LENGTH_SHORT).show();
}
});
먼저 플레이 준비가 되면 videoView.start() 메서드로 동영상 플레이를 시작한다.
다음으로 vieoView.getDuration() 메서드를 이용하여 전체 동영상 플레이 시간을 받아서 분/초로 변환한후 출력하고, SeekBar의 Max값을 총 플레이 시간으로 저장한다. 그리고 100ms마다 seekBar의 플레이 상태를 이동시키기 위해서 updateHandler에 updateVideoTime 함수를 100ms 이후에 실행하도록 지정한다.
updateVideoTime 함수는 다음과 같다.
// seekBar를 이동시키기 위한 쓰레드 객체
// 100ms 마다 viewView의 플레이 상태를 체크하여, seekBar를 업데이트 한다.
private Runnable updateVideoTime = new Runnable(){
public void run(){
long currentPosition = videoView.getCurrentPosition();
seekBar.setProgress((int) currentPosition);
updateHandler.postDelayed(this, 100);
}
};
만약에 안드로이드 에뮬레이터에서 실행을 하게 되면 일부 MP4 동영상은 Decoder를 찾을 수 없다는 에러가 나올 수 있는데, 이는 MP4 확장명을 가지더라도, 내부 인코딩 방식이 각기 틀리기 때문에, 안드로이드 에뮬레이터에서는 일부 디코더만 지원하기 때문에, 제대로 플레이가 되지 않을 수 있다. 실제 기기를 연결하면, 실 기기에는 대부분의 MP4 디코더가 탑재되어 있기 때문에, 문제 없이 동영상이 플레이 된다.
그렇지만 실제로 동영상을 서비스하는 플레이어를 만들려면, 동영상을 플레이어에 최적화된 형태로 다시 인코딩을 해서 플레이를 하는 것이 좋다. 화면 해상도에 따른 품질 차이나, 압축 방식 그리고 초당 프레임수 (FPS)등에 따라서 영향을 받기 때문에 최적의 인코딩 포맷을 찾은 후에, 그 포맷에 따라서 인코딩을 하는 것이 좋으며, 이런 인코딩 전문 플랫폼으로는 예전 엠군에서 제공하는 위켄디오나 해외의 zencoder등과 같인 인코딩 플랫폼을 사용하면 손쉽게 연동이 가능하다.
참고 자료
'프로그래밍 > 안드로이드' 카테고리의 다른 글
안드로이드 채팅 UI 만들기 #1 - ListView를 이용한 채팅창 만들기 (8) | 2015.10.13 |
---|---|
안드로이드 Fragment 이해하기 (0) | 2015.10.06 |
SharedPreference를 이용한 데이타 저장 및 애플리케이션간 데이타 공유 (0) | 2015.09.23 |
안드로이드 웹뷰(Webview)의 이해와 성능 최적화 방안 (3) | 2015.09.16 |
안드로이드에서 REST API 호출하기 (2) | 2015.09.15 |