Home » Android -14. 멀티미디어 처리

Android -14. 멀티미디어 처리

14.1. 오디오 재생

  • MediaPlayer 클래스는 오디오/비디오 파일 재생과 스트림을 제어 할 수 있다.

<step12/AudioPlaySample>
[php]
public void audioPlay() {
if(player != null) { (1)
player.stop();
player.release();
player = null;
} else {
// 객체 생성
player = new MediaPlayer(); (2)
}
try {
// 음악 path 설정
// 지금처럼 프로젝트 내에 있을 때는 player.create(this, R.raw.filename); 으로 해도 가능
player.setDataSource(AudioPlaySample.this, audioPath); (3)

player.prepare(); (4)
// 음악 재생
player.start(); (5)
ToastUtil.show(AudioPlaySample.this, "음악이 재생되었습니다.");
} catch (Exception e) {
e.printStackTrace();
}

}

public void audioStop() {
if(player == null) {
return;
} else {
// 음악 정지
player.stop();
player.release();
player = null;
ToastUtil.show(AudioPlaySample.this, "음악이 중지되었습니다.");
}
}

@Override
protected void onPause() { (6)
if(player != null) {
player.stop();
player.release();
player = null;
}

super.onPause();
}
[/php]

번호 설명
(1) 플레이어가 만들어져 있는 상태라면 재생중지 및 리소스 해제
(2) 플레이어 생성
(3) 재생 될 파일의 경로 설정
(4) 미디어 준비
(5) 재생 시작
(6) Activity가 onPause가 될 때 플레이어가 만들어져 있는 상태라면 재생 중지 및 리소스 해제

14.2. 오디오 녹화

  • MediaRecorder는 녹음, 녹화를 할 수 있도록 도와준다.
    [php]
    public void recStart() {
    if (recorder != null) { (1)
    recorder.stop();
    recorder.release();
    recorder = null;
    }

    // 실험 결과 왠만하면 아래 recorder 객체의 속성을 지정하는 순서는 이대로 하는게 좋다 위치를 바꿨을때 에러가 났었음
    // 녹음 시작을 위해 MediaRecorder 객체 recorder를 생성한다.
    recorder = new MediaRecorder(); (2)

    // 오디오 입력 형식 설정
    recorder.setAudioSource(MediaRecorder.AudioSource.MIC); (3)
    // 음향을 저장할 방식을 설정
    recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); (4)

    // 오디오 인코더 설정
    recorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT); (5)

    // 저장될 파일 지정
    recorder.setOutputFile(recFile); (6)

    try {
    ToastUtil.show(AudioRecSample.this, "녹음이 시작되었습니다.");

    // 녹음 준비,시작
    recorder.prepare(); (7)
    recorder.start(); (8)
    } catch (Exception ex) {
    Logger.i(ex.toString());
    }

    }

    public void recStop() {
    if (recorder == null)
    return;

    // 녹음을 중지
    recorder.stop(); (9)

    // 오디오 녹음에 필요한 메모리를 해제한다
    recorder.release(); (10)
    recorder = null;

    ToastUtil.show(AudioRecSample.this, "녹음이 중지되었습니다.");
    }
    [/php]

    번호 설명
    (1) 녹음 시작시 레코더가 사용 중일 때 녹음중지 및 리소스 해제
    (2) 미디어 레코더 객체 생성
    (3) 오디오 입력 형식 설정
    (4) 음향을 저장할 방식을 설정
    (5) 오디오 인코더 설정
    (6) 저장될 파일 지정
    (7) 녹음 준비
    (8) 녹음 시작
    (9) 녹음 중지
    (10) 녹음 리소스 해제

14.3. 비디오 재생

<videoplay_sample.xml>
[php]
<VideoView
android:id="@+id/videoPlaySampleVideoView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp" />
[/php]
<step12/VideoPlaySample>
[php]
videoPlaySampleVideoView = (VideoView) findViewById(R.id.videoPlaySampleVideoView);
… 생략
// 동영상 패스 설정
// 경로가 웹일 경우
//videoPlaySampleVideoView.setVideoURI(Uri.parse(VIDEO_URL));

// sd카드 일 경우

// 동영상의 경로가 SD Card 에 있다면 아래와 같이 설정
//String path = Environment.getExternalStorageDirectory() + "/TestVidio6.mp4";

// raw 폴더 일 경우
Uri audioPath = Uri.parse("android.resource://com.ahope.tutorial/"+ R.raw.sample_video);
videoPlaySampleVideoView.setVideoURI(audioPath); (1)

// 미디어컨트롤러 추가하는부분
mController = new MediaController(this); (2)
videoPlaySampleVideoView.setMediaController(mController); (3)

videoPlaySampleVideoView.requestFocus(); (4)
… 생략

private void playVideo() {
// 비디오를 처음부터 재생할땐 0
videoPlaySampleVideoView.seekTo(0); (5)
// 비디오 재생 시작
videoPlaySampleVideoView.start(); (6)
}

private void stopVideo() {
// 비디오 재생 잠시멈춤
videoPlaySampleVideoView.pause(); (7)
// 비디오 재생 완전 멈춤
videoPlaySampleVideoView.stopPlayback(); (8)
}
[/php]

번호 설명
(1) 파일 경로 설정
(2) 컨트롤러 생성
(3) 비디오 뷰에 컨트롤러 설정
(4) 비디오뷰에 포커스를 준다.
(5) 비디오의 시크바 설정 0이면 처음부터 재생
(6) 재생 시작
(7) 일시 정지
(8) 완전 정지

14.4. 비디오 녹화

<videorec_sample.xml>
[php]
<SurfaceView
android:id="@+id/videoRecSampleSurfaceView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="3.5" />
[/php]
* surfaceView에 대해서는 다음 카메라에서 자세히 설명할 예정

  • <퍼미션 등록>
    <AndroidManifest.xml>
    [php]
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    [/php]
    <step12/VideoRecSample>
    [php]
    // SurfaceView 클래스 객체를 이용해서 카메라에 받은 녹화하고 재생하는데 쓰일것이다.
    videoRecSampleSurfaceView = (SurfaceView) findViewById(R.id.videoRecSampleSurfaceView); (1)
    // SurfaceView 클래스를 컨트롤하기위한 SurfaceHolder 생성
    holder = videoRecSampleSurfaceView.getHolder(); (2)
    … 생략
    public void recStart() {
    try {
    // 녹화 시작을 위해 MediaRecorder 객체 recorder를 생성한다.
    if (recorder == null) {
    recorder = new MediaRecorder();
    }
    setCamera();
    recorder.setCamera(camera); (3)

    // 오디오와 영상 입력 형식 설정 (4)
    recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
    recorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
    recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);

    // 오디오와영상 인코더 설정 (5)
    recorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
    recorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT);

    // 저장될 파일 지정
    filename = createFilename();
    recorder.setOutputFile(filename);

    // 녹화도중에 녹화화면을 뷰에다가 출력하게 해주는 설정
    recorder.setOrientationHint(90); (6)
    recorder.setPreviewDisplay(holder.getSurface()); (7)

    // 녹화 준비,시작
    recorder.prepare();
    recorder.start();

    } catch (Exception ex) {
    ex.printStackTrace();
    recorder.release();
    recorder = null;
    }
    }

    public void recStop() {
    if (recorder == null)
    return;
    // 녹화 중지
    recorder.stop();
    // 영상 재생에 필요한 메모리를 해제한다.
    recorder.release();
    recorder = null;

    ContentValues values = new ContentValues(10); (8)

    values.put(MediaStore.MediaColumns.TITLE, "RecordedVideo");
    values.put(MediaStore.Audio.Media.ALBUM, "Video Album");
    values.put(MediaStore.Audio.Media.ARTIST, "Mike");
    values.put(MediaStore.Audio.Media.DISPLAY_NAME, "Recorded Video");
    values.put(MediaStore.MediaColumns.DATE_ADDED,
    System.currentTimeMillis() / 1000);
    values.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
    values.put(MediaStore.Audio.Media.DATA, filename);

    Uri videoUri = getContentResolver().insert( (9)
    MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
    if (videoUri == null) {
    Logger.i("Video insert failed.");
    return;
    }
    camera.lock();
    camera.release();
    sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, (10)
    videoUri));
    }
    [/php]

    번호 설명
    (1) SurfaceView 객체 생성
    (2) SurfaceView 클래스를 컨트롤하기위한 SurfaceHolder 생성
    (3) 녹화를 할 카메라 설정
    (4) 오디오와 영상 입력 형식 설정
    (5) 오디오와영상 인코더 설정
    (6) 안드로이드의 카메라는 기본 가로모드이기 때문에 세로로 했을 경우 90도 돌려줘야 제대로 표시 됨
    (7) 녹화도중에 녹화화면을 뷰에 출력하게 해주는 설정
    (8) 녹화한 파일을 저장하기 위한 설정
    (9) 설정한 값을 Uri로 만들어줌
    (10) sendBroadcast를 이용하여 지정된 경로에 파일 저장

14.5. 카메라

SurfaceView에 대하여

  • SufraceView라는 이름에서 알 수 있듯이, TextView, ImageView처럼 컨텐츠를 표시할 수 있는 View 중 하나이다. 하지만 이 SurfaceView는 다른 View들과는 달리 직접 SurfaceView가 컨텐츠를 표시하지 않는다.
  • 일반적인 View는 화면에 뷰를 표시하는 연산과 기타 연산, 사용자와의 상호작용 처리 등이 모두 하나의 쓰레드에서 처리된다. 이것을 가장 잘 확인할 수 있는 예가 바로 ANR(Application Not Responding)이다. ANR은 어플리케이션이 5초 이상 동작을 멈췄을 때, 조금 더 자세히 말하자면 GUI업데이트가 5초이상 멈췄을 때 발생하는데, 실제로 가끔씩 몇몇 어플리케이션에서 연산이 늦어지면 GUI 업데이트가 늦어지며 ANR이 발생하는 것을 볼 수 있다. 즉, GUI 업데이트와 다른 연산이 같은 쓰레드 내에서 처리되기에 이런 현상이 발생하는 것이다.

  • 그런데, 우리가 처리해야할 것은 카메라 프리뷰, 즉 실시간으로 화상을 카메라로부터 받아서 1초에 몇십프레임 이상의 속도로 화면을 업데이트해야 하는 동작이다. 그렇기에 만약 SurfaceView를 쓰지 않으면 뷰를 업데이트하는데 쓰레드의 자원을 모두 써서 어플리케이션의 정상적인 동작을 보장하기 어렵다.

  • 그래서 등장한 것이 바로 SurfaceView이다. SurfaceView는 화면 업데이트를 백그라운드 쓰레드로 수행하여 어플리케이션의 자원을 잠식하지 않고 원활하게 뷰를 업데이트 해 준다. 뿐만 아니라 SurfaceView는 OpenGL을 통한 가속이 지원되어 원활한 3D그래픽 표현도 가능하다.

SurfaceView의 구조

  • ImageView나 TextView는 뷰 자체가 바로 컨텐츠를 표시하지만, SurfaceView는 조금 복잡한 구조를 가지고 있다.
  • SurfaceView 자체는 하나의 “틀” 역할만 하고, 실제로 컨텐츠가 표시되는 곳은 SurfaceView 내의 Surface 객체다.


그림 49. SurfaceView의 구조

  • 위의 그림에서 알 수 있듯이, SurfaceHolder객체가 실제 Surface에 접근하여 화면을 처리해주는 구조를 가지고 있다. SurfaceHolder라는 이름 그대로, Surface 객체를 잡고(Hold) 관리해주는 것이라 보면 된다.

  • <퍼미션 등록>
    <AndroidManifest.xml>
    [php]
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />

    <camera_sample.xml>
    <com.ahope.tutorial.activity.step12.CameraSamplePreview (1)
    android:id="@+id/cameraSamplePreview"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="3" />
    [/php]

    번호 설명
    (1) SurfaceView를 상속받아 구현한 클래스

<step12/CameraSamplePreview>
[php]
public class CameraSamplePreview extends SurfaceView implements SurfaceHolder.Callback { (1)
… 생략
public CameraSamplePreview(Context context, AttributeSet attrs) {
super(context, attrs);
// SurfaceHolder.Callback을 설정함으로써 Surface가 생성/소멸되었음을 알 수 있다.
mHolder = getHolder(); (2)
mHolder.addCallback(this); (3)
this.context = context;
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { (4)
// 표시할 영역의 크기를 알았으므로 해당 크기로 Preview를 시작한다.
Camera.Parameters parameters = mCamera.getParameters();
parameters.setPreviewSize(width, height);
Display display =
((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
if(display.getRotation() == Surface.ROTATION_0) (5)
{
mCamera.setDisplayOrientation(90);
}

if(display.getRotation() == Surface.ROTATION_90)
{
}

if(display.getRotation() == Surface.ROTATION_180)
{
}

if(display.getRotation() == Surface.ROTATION_270)
{
mCamera.setDisplayOrientation(180);
}

List<Size> arSize = parameters.getSupportedPreviewSizes();
if (arSize == null) {
parameters.setPreviewSize(width, height);
} else {
int diff = 10000;
Size opti = null;
for (Size s : arSize) {
if (Math.abs(s.height – height) < diff) {
diff = Math.abs(s.height – height);
opti = s;

}
}
}
parameters.setJpegQuality(100); (6)

mCamera.setParameters(parameters); (7)
mCamera.startPreview(); (8)

}

@Override
public void surfaceCreated(SurfaceHolder holder) { (9)
// Surface가 생성되었다면, 카메라의 인스턴스를 받아온 후 카메라의 Preview 를 표시할 위치를 설정.
mCamera = Camera.open(); (10)
try {
mCamera.setPreviewDisplay(holder); (11)
} catch (IOException exception) {
mCamera.release(); (12)
mCamera = null;
}
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
/*
다른 화면으로 돌아가면, Surface가 소멸. 따라서 카메라의 Preview도 중지해야 한다.
카메라는 공유할 수 있는 자원이 아니기에, 사용하지 않을 경우
-액티비티가 일시정지 상태가 된 경우 등 – 자원을 반환해야한다.
*/
mCamera.stopPreview(); (13)
mCamera.release(); (14)
mCamera = null;

}
[/php]

번호 설명
(1) SurfaceView 상속 받고 SurfaceHolder.Callback 구현
(2) Surface를 관리할 holder 받아옴
(3) 콜백 설정
(4) Surface가 바뀔 때 마다 호출 되는 메소드
(5) 카메라의 로테이션 설정
(6) 카메라로 캡쳐가 이루어질 때의 jpeg 퀄리티 설정
(7) 설정한 카메라 설정을 적용
(8) 카메라 프리뷰 시작
(9) Surface가 만들어질 때 호출 되는 메소드
(10) 카메라 인스턴스를 받아옴
(11) 카메라의 프리뷰를 표시할 위치 설정
(12) Exception 발생 시 카메라 리소스 해제
(13) 카메라 프리뷰 정지
(14) 카메라 리소스 해제

<step12/CameraSample>
[php]
preview = (CameraSamplePreview) findViewById(R.id.cameraSamplePreview); (1)
… 생략
//sets what code should be executed after the picture is taken
Camera.PictureCallback mCall = new Camera.PictureCallback() { (2)
@Override
public void onPictureTaken(byte[] data, Camera camera)
{
Bitmap bmp = BitmapFactory.decodeByteArray(data, 0, data.length); (3)

cameraSampleImgView.setImageBitmap(bmp);
}
};

// 카메라에서 이미지 가져오기
public void capture() {
new TakePictureTask().execute(); (4)
}

private class TakePictureTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void… params) {
preview.mCamera.takePicture(null, null, mCall); (5)
try {
Thread.sleep(1000); // 3 second preview (6)
} catch (InterruptedException e) {
preview.mCamera.stopPreview();
preview.mCamera.release();
onRestart();
}
return null;
}

@Override
protected void onPostExecute(Void result) {
try {
preview.mCamera.startPreview(); (7)
} catch(Exception e) {
preview.mCamera.stopPreview();
preview.mCamera.release();
onRestart();
}

}
[/php]

번호 설명
(1) SurfaceView 객체 생성
(2) 캡쳐 시 콜백 구현
(3) 캡쳐한 이미지를 비트맵으로 만듬
(4) 캡쳐 작업을 위한 TakePictureTask 실행
(5) 화면 캡쳐
(6) 1초 지연
(7) 1초 지연 후 다시 프리뷰를 실행

14.6. 바코드 스캐너

  • 바코드를 관련해서는 Zxing 라이브러리를 사용하기 때문에 Zxing 라이브러리를 이용한 바코드 인식 샘플을 만드는 과정이다.

(1) Zxing 라이브러리 다운로드

https://github.com/zxing/zxing 접속 후 다운로드


그림 50. Zxing 다운로드

(2) 압축 해제 후 core lib를 프로젝트의 libs 폴더에 넣어준다.

  • 최근 Zxing은 jar파일이 없어 core의 패키지들을 넣어주거나 라이브러리 프로젝트로 만들어 사용한다. 샘플 프로젝트에 미리 구해둔 jar 파일이 있으므로 그것을 사용해도 무방하다.


그림 51. Zxing jar 파일 추가

(3) 그림의 패키지들과 소스들을 프로젝트에 추가


그림 52. Zxing 필요한 패키지 및 소스 추가

  • 여기까지 진행하면 아마 zxing 관련 패키지들에 에러들이 많이 뜨게 될텐데 이는 R.java의 경로가 맞지 않거나 리소스 파일들이 없어서이다. R.java의 경로는 바꿔 주도록 하고, 필요한 리소스는 아래와 같으므로 각 경로에 추가하도록 한다.

(4) 필요한 리소스 추가

drawable/share_via_barcode.png
layout/capture
layout/encode
layout/help
layout/search_book_contents_header
layout/search_book_contents_lsit_item
layout/search_book_contents
layout/share
menu/capture
menu/history
menu/encode
raw/transkey_tock
raw/beep.ogg
values 디렉토리
xml 디렉토리

<step12/BarcodeSample>
[php]
IntentIntegrator integrator = new IntentIntegrator(BarcodeSample.this); (1)
integrator.initiateScan(); (2)

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); (3)
if (scanResult != null) {
barcodeSampleFormat.setText(scanResult.getFormatName()); (4)
barcodeSampleContent.setText(scanResult.getContents()); (5)
} else {
ToastUtil.show(BarcodeSample.this, "스캔 결과가 없습니다.");
}}
[/php]

번호 설명
(1) Barcode 스캐너 호출을 위한 객체 생성
(2) 바코드 스캐너 호출
(3) 바코드 스캔 후 돌아 왔을 때 결과 값을 가진 객체
(4) 스캔 이름
(5) 스캔 내용


그림 53. 바코드