블로그 이미지
우주청년
집구석 음악가, 잡식성 프로그래머

calendar

            1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30            

Notice

2011.04.04 20:09 문서/프로그래밍

#3에 이어서 원래는 "세부 조절 옵션 넣기"인데, 아무래도 이 부분은 모든 작업이 완료되고 작업하는게 맞는 것 같습니다. 그래서 이번에는 "로컬 동영상 재생 + 로컬 자막"부분을 작업 해 보도록 합시다.

[할일]

 - 파일 리스트 꾸미기
 - 로컬 동영상 재생 + 로컬 자막

 - 동영상 스트리밍 + 외부 자막
 - 세부 조절 옵션 넣기
 - 에러 잡기

[본문]

  일단 동영상을 재생하기 위해서는 "플레이어 컨트롤 자체를 만드느냐 vs 기본 제공되는 컨트롤을 사용하느냐"로 나누어 지겠는데요. 편의상 "기본 제공되는 컨트롤을 사용"하는 것으로 가겠습니다.
  그리고 기본 제공되는 컨트롤에도 종류가 있는데, SurfaceView를 이용해서 만드는 방법과 SurfaceView를 상속해서 만들어진 VideoView를 이용해서 만드는 방법이 있습니다. 편한 것을 좋아하는 저는 "VideoView"를 사용하겠습니다.
  동영상 재생이 원할하게 된다면, 마지막으로 "Smi 자막 파일"을 읽어와서 화면에 뿌려주는 부분을 작업해야 합니다. 아직 한국 웹에는 관련 내용을 다루는 포스트가 없고, 외국 웹에는 너무 복잡하게 만들어져 있기에, 우리는 Smi 자막을 해부해서 동영상 재생기에 적용 할 것입니다.
  세부적인 내용을 보고 본격적으로 작업에 들어 가 봅시다.

  1. 동영상 재생을 위한 엑티비티 생성
  2. 동영상 재생 레이아웃 디자인
  3. 커스텀 리스트에서 선택한 동영상 파일 받아오기
  4. 동영상 재생하기
  5. Smi 자막 파일 읽어오기
  6. 자막을 동영상 재생 시간에 맞춰서 출력하기

이렇게 6단계로 작업을 진행합니다.

[소스 파일과 어플을 첨부합니다. 내용에 참고하세요.]


1. 동영상 재생을 위한 엑티비티 생성

저번에 만들어 놓았던 커스텀 리스트의 엑티비티와는 별도로 동영상 재생을 위한 엑티비티를 만들어 보겠습니다.

일단 소스 폴더에 video.java 파일을 만들고, 클래스 이름을 video로 정해줬습니다.

그리고 매니패스트의 Application -> Application Nodes -> Add 버튼 클릭 -> Activity 선택 후 확인을 하고
해당 엑티비티의 선택하면 나오는 오른쪽 폼의
(1) Name에 있는 Browse를 눌러서 방금 만든 video클래스를 선택합니다.
(2) Theme에 @android:style/Theme.NoTitleBar.Fullscreen 를 입력합니다.
     (타이틀 바를 숨기고 전체화면을 이용하기 위한 옵션입니다.)
(3) Screen orientation에 landscape를 선택합니다.
     (세로모드로만 재생 할 수 있게 만들 생각입니다.)

  일단 이걸로 엑티비티 생성은 끝났습니다. 저는 GUI로 작업하는 것을 좋아합니다. xml로 작업하시는 분들은 소스 파일을 받으셔서 참고하세요.


2. 동영상 재생 레이아웃 디자인

  엑티비티를 생성했으니, 이제 동영상 재생을 위한 레이아웃을 디자인 해 봅시다. 솔직히 디자인이라고 할것도 없이 프레임 레이아웃에 텍스트뷰와 비디오뷰를 추가하면 끝입니다.

<videoview.xml>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent" android:orientation="horizontal">
    <FrameLayout android:layout_width="wrap_content" android:id="@+id/frameLayout1" android:layout_height="fill_parent">
        <VideoView android:layout_width="fill_parent" android:keepScreenOn="true" android:layout_height="fill_parent" android:id="@+id/videoView1" android:layout_gravity="center"></VideoView>
        <TextView android:text="TextView" android:layout_height="wrap_content" android:id="@+id/subtitle" android:textSize="24dip" android:shadowColor="#000000" android:shadowDx="0" android:shadowDy="0" android:shadowRadius="2" android:textColor="#FFFFFF" android:layout_width="wrap_content" android:layout_gravity="bottom|center" android:gravity="center"></TextView>
    </FrameLayout>
</LinearLayout>

다만, 자막을 표시 할 텍스트뷰는 아랫쪽 중앙에 표시하고, 내부 텍스트 정렬을 중앙으로 설정했습니다.


3. 커스텀 리스트에서 선택한 동영상 파일 받아오기

  앞서 만들었던 커스텀 리스트에서 동영상 타입을 선택하면 비디오 재생 엑티비티로 전달하기 위한 코드를 작성 해 봅시다. main클래스의 fileList.setOnItemClickListener 부분을 아래와 같이 수정 해 줍니다.
fileList.setOnItemClickListener(new OnItemClickListener() {
        	public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        		ListData item = arrayList.get(position);
        		if(item.gettype() == 0) {
        			File dir = new File(nPath + item.getname() + "/");
    				if(dir.isDirectory() && dir.canRead()) {
    					PathList_prev.add(nPath);
    					PathList_next.clear();
    					nPath = dir.getAbsolutePath().toString();
    					if(!nPath.endsWith("/")) {
    						nPath += "/";
    					}
    					updateFileList(nPath);
    				} else {
    					errorDlg.setMessage("폴더가 없거나 읽을 수 없습니다.").show();
    				}
        		} else if(item.gettype() == 1) {
        			File file = new File(nPath + item.getname());
        			if(file.isFile() && file.canRead()) {
 						Intent intent = new Intent(main.this, video.class);
 						intent.putExtra("FilePath", file.toString());
 					    startActivity(intent);
 					} else {
    					errorDlg.setMessage("폴더가 없거나 읽을 수 없습니다.").show();
    				}
        		}
        	}
        });


실제로 추가된 구문은 타입이 동영상인지 확인하는 부분과 비디오 재생 엑티비티에 동영상 파일 경로를 넘겨주고, 실행하는 부분 밖에 없습니다.

Intent intent = new Intent(main.this, video.class);
 -> video클래스로 연결되는 새 인텐트를 만들고
intent.putExtra("FilePath", file.toString());
 -> FilePath라는 이름으로 동영상 파일 경로를 넘겨주고
startActivity(intent);
 -> 동영상 재생 엑티비티를 실행합니다.

이제 video클래스에서 넘겨준 데이터를 받아봅시다.

Intent intent = getIntent();
 -> 값을 받기 위한 인텐트를 만들고
String path = intent.getStringExtra("FilePath");
 -> FilePath라는 이름에서 동영상 경로를 넘겨 받습니다.


생각보다 간단하게 엑티비티 간 데이터 전달이 이루어 집니다.


4. 동영상 재생하기

  이제 받아온 동영상 파일 경로를 VideoView로 넘겨주고 동영상을 재생 해 봅시다. 우선 동영상을 재생하는 VideoView와 자막을 표시할 TextView의 ID를 받아오고, 재생을 컨트롤 해줄 mediaController를 생성 해 보겠습니다.

videoView = (VideoView)findViewById(R.id.videoView1);
subtitle = (TextView)findViewById(R.id.subtitle);

subtitle.setText("");

MediaController mediaController = new MediaController(this);
mediaController.setAnchorView(videoView);

videoView.setMediaController(mediaController);
videoView.setVideoPath(path);
videoView.requestFocus();
videoView.start();

일단은 이렇게만 해주면 동영상 재생은 끝이 납니다. 생각보다 소스가 간단합니다.


5. Smi 자막 파일 읽어오기

  Smi 자막 파일을 리더를 이용해서 읽어 봅시다. 우선 자막 파일을 위한 데이터 형을 하나 정의합니다.
class smiData {
	long time;
	String text;
	smiData(long time, String text) {
		this.time = time;
		this.text = text;
	}
	
	public long gettime() {
		return time;
	}
	
	public String gettext() {
		return text;
	}
}

  long 타입으로 된 싱크 시간과, String으로 된 텍스트로 구성되어 있습니다. 그리고 video클래스에 아래의 변수를 선업해 둡니다.

private ArrayList<smiData> parsedSmi;
private boolean useSmi;

  이제 자막 파일을 읽어 올텐데, 파일은 동영상 재생 이전에 미리 읽어 둬야 합니다. 따라서, 위에서 만든 동영상 재생 소스의 앞에 내용을 추가합니다.
        String smiPath = path.substring(0,path.lastIndexOf(".")) + ".smi";
        File smiFile = new File(smiPath);
        if(smiFile.isFile() && smiFile.canRead()) {
        	useSmi = true;
        	parsedSmi = new ArrayList<smiData>();
        	try {
        		BufferedReader in = new BufferedReader(new InputStreamReader(
        				new FileInputStream(new File(smiFile.toString())),"MS949"));

        		String s;
        	    long time = -1;
        	    String text = null;
        	    boolean smistart = false;
        	    
        	    while ((s = in.readLine()) != null) {
        	    	if(s.contains("<SYNC")) {
        	    		smistart = true;
        	    		if(time != -1) {
        	    			parsedSmi.add(new smiData(time, text));
        	    		}
        	    		time = Integer.parseInt(s.substring(s.indexOf("=")+1, s.indexOf(">")));
        	    		text = s.substring(s.indexOf(">")+1, s.length());
        	    		text = text.substring(text.indexOf(">")+1, text.length());
        	    	} else {
        	    		if(smistart == true) {
        	    			text += s;
        	    		}
        	    	}
        	    }
        	    in.close();
        	} catch (IOException e) {
        	    System.err.println(e);
        	    System.exit(1);
        	}
        } else {
        	useSmi = false;
        }

  동영상 파일의 확장자를 없애고 .smi를 추가한 후 파일을 읽고, 파일이 없으면 자막 사용을 하지 않는 것으로 합니다. 만약 파일이 있다면, 한줄씩 자막 파일을 읽으면서 "<SYNC"가 있는 줄에서 싱크 시간을 분리하고,
<SYNC ~> 구문 다음부터 임시 String 변수 text에 저장을 하다가 다음 "<SYNC"를 만나면 해당 싱크 시간 자막의 끝 부분이라고 생각하고 싱크 시간과 텍스트를 SmiData형 배열에 저장합니다.
  이렇게 끝까지 자막 파일을 읽고 나면, SmiData형 배열에 싱크시간/텍스트로 분리된 데이터가 저장됩니다.


6. 자막을 동영상 재생 시간에 맞춰서 출력하기

  자막을 읽어 왔으니 이제 동영상 재생 시간에 맞춰서 자막을 출력 해 봅시다. 우리는 쓰레드를 이용해서 백그라운드로 0.3초마다 해당 재생시간에 맞는 자막을 찾아서 출력을 해줄 것입니다.

<쓰레드 생성 및 실행 부분>
        if(useSmi == true) {
        	new Thread(new Runnable() 
            {
            	public void run() 
            	{
            		try {
            			while(true) {
            				Thread.sleep(300);
            				myHandler.sendMessage(myHandler.obtainMessage());
            			}
            		} catch (Throwable t) {
            			// Exit Thread
            		}
            	}
            }).start();
        }

<내용을 처리 할 핸들러 부분>
    Handler myHandler = new Handler()
    {
    	public void handleMessage(Message msg)
    	{
    		countSmi = getSyncIndex(videoView.getCurrentPosition());
    		subtitle.setText(Html.fromHtml(parsedSmi.get(countSmi).gettext()));
    	}
    };

<싱크 값을 찾는 2진 검색 함수 부분>
    public int getSyncIndex(long playTime) {
    	int l=0,m,h=parsedSmi.size();
    	
    	while(l <= h) {
    		m = (l+h)/2;
    		if(parsedSmi.get(m).gettime() <= playTime && playTime < parsedSmi.get(m+1).gettime()) {
    			return m;
    		}
    		if(playTime > parsedSmi.get(m+1).gettime()) {
    			l=m+1;
    		} else {
    			h=m-1;
    		}
    	}
    	return 0;
    }

동작을 정리 해 보면,

  (1) 0.3초 마다 쓰레드에서 핸들러로 메세지를 보낸다.
  (2) 핸들러에서 getSyncIndex를 호출 한다. (매개 변수는 해당 동영상의 현재 재생 시간)
  (3) 2진 검색으로 해당 매개변수에 맞는 싱크 값이 들어 있는 인덱스를 찾고 리턴한다.
  (4) 자막 데이터에서 해당 인덱스의 텍스트를 텍스트뷰에 출력한다.

이렇게 4가지로 구성 되어 있습니다.


[마치면서]

  이제 커스텀 리스트와 동영상 재생 그리고 자막 표시까지 끝냈으니, 간단한 "동영상 플레이어"라고 부를 수 있을 정도 까지 온 것 같습니다. 하지만 현재 만들어 놓은 Smi 자막을 파싱하는 소스는 다중 언어를 지원하지 않습니다. 다중 언어로 이루어진 Smi파일을 열어서 구성을 살펴보시면 해당 소스를 쉽게 다중 언어에 맞게 수정 하실 수 있을 듯 합니다. 다음 장에서는 웹에서 동영상을 스트리밍 해보고, 웹 브라우저의 동영상 링크에 플레이어가 동작 할 수 있게 소스를 추가 해 봅시다.

  이번 포스트의 자세한 내용은 본문 들어가기전에 첨부 해놓은 소스를 열어서 확인 해보시고, 모르는 내용이나 궁금한 점이 있으면 댓글로 달아주세요.
posted by 우주청년

댓글을 달아 주세요

  1. zzin 2011.06.14 18:16  Addr Edit/Del Reply

    코드 열심히 잘 공부하겠습니다. 큰 도움이 되었습니다. 정말 감사해요~~

  2. GSTAR 2011.07.30 11:54  Addr Edit/Del Reply

    감사합니다.
    근데 km에서는 제대로 나오는 자막이 깨져서 나오네요~
    자막파일의 엔코딩때문인지..
    자막을 제가 직접 편집했거든요.
    utf8로 되어야 하는가요?

    • Favicon of https://gsroom.tistory.com 우주청년 2011.07.30 22:54 신고  Addr Edit/Del

      new File(smiFile.toString())),"MS949")로 받아와서
      부호화는 ANSI, 언어코드는 MS949로 된 파일만 정상적으로 보입니다.
      소스 앞 부분을 수정해서 파일의 인코딩에 맞게 읽어오게 수정하시면 됩니다.

  3. GSTAR 2011.07.31 08:43  Addr Edit/Del Reply

    감사합니다. 복받으실거예요~~

  4. ethyo 2011.11.02 03:32  Addr Edit/Del Reply

    정말로 복받으실듯요.. 실력자...^^

  5. 저는 도울정보기술의 연구소장입니다.
    저희회사에서는 안드로이드로 동영상을 플레이 해볼랴고 하는데
    어플형태로 되었으면 합니다.
    동영상 영역 밖 위 아래에 다른 선택 메뉴버튼 기능을 두려 하고요
    개발비를 드릴것입니다.
    답변부탁합니다.
    제 미일은 doulzzang@gmail.com 입니다.
    그럼

  6. 미카사 2012.02.17 20:01  Addr Edit/Del Reply

    서피스 뷰로 비디오를 바꾸는데, 자막 처리 부분에 궁금증이 있어서 문의해봅니다.

    지금도 댓글 보시나 모르겠어요ㅎㅎ

    예제에서, parsedSmi에서 시간을 불러오는 방법을 모르겠네요.

    시간을 불러와서 currentPlayTime과 비교해서 맞을 경우에만 출력하도록 하고싶은데,

    이 소스 대로 사용할 경우, 시작하자마자 맨 처음 자막이 출력되더군요...

    보통 맨 처음 부분엔 카피라이트를 표기하니 크게 문제될 부분이 없지만,

    아닐 경우엔 영 어색해서...

    • Favicon of https://gsroom.tistory.com 우주청년 2012.02.18 14:36 신고  Addr Edit/Del

      강좌용으로 간단하게 구현한거라 예외상황을 고려하지 않고 작업한 소스입니다.

      싱크 시간대에 자막 데이터가 없으면 getSyncIndex 에서 0을 리턴하게 소스를 짜놨는데, 이 부분을 -1을 리턴하게 수정하시고 리턴값이 -1일경우 공백을 출력해주시면 원하시는 결과를 얻으실 수 있습니다.

      그리고 자막 데이터인 smi 파일에서 시간을 불러오는 부분은 이 게시글에 5번 부분 소스를 참고하시면 됩니다.

  7. 아기달링 2012.04.12 12:01  Addr Edit/Del Reply

    이렇게 자신이 만든 기술을 공개해주신것에 대해 감사드립니다. ㅎㅎ
    그 덕분에 저 같은 초급 개발자들이 희망을 얻습니다. ㅎㅎ
    앞으로도 좋은글 부탁드립니다.

  8. 이동훈 2012.04.12 17:26  Addr Edit/Del Reply

    아직두 보실려는지..잘 모르겠네요..
    잘 받아서 열심히 공부하고 있는데..우선 감사합니다...
    컴파일이나..재생시 에러는 전혀 없고..컨트롤 박스상으로는 열심히 재생이 되고 있는데
    화면이 안보이네요...제가 초보라...머가 잘못 되었는지..모르겠네요...
    수고하세요..

  9. 2015.10.01 19:28  Addr Edit/Del Reply

    비밀댓글입니다