2023.03.08 - 안드로이드 앱개발자 과정

CHA·2023년 3월 9일
0

Android



CameraX API

CameraX API 을 사용하기 위해서 개발자 사이트를 참고하면 좋습니다. 사용방법이 그대로 나와있으니, 참고해서 만들어봅시다.
CameraX - Android 개발자 사이트

이번 테스트에서 우리가 할 작업은 다음과 같습니다.

첫째. CameraX API 를 사용하기 위한 준비작업을 합니다.
둘째. 카메라 관련 동적 퍼미션을 설정합니다.
셋째. 프리뷰 기능을 구현합니다.
넷째. 캡처 기능을 구현합니다.


Dependency 추가하기 & PreviewView 만들기

CameraX 를 사용하기 위해 일단 위 사진의 Dependency 를 추가하라고 합니다. 추가해줍시다.

그리고 카메라를 사용할때 나오는 화면을 구성하기 위한 PreviewView 를 만들어줍니다.

<androidx.camera.view.PreviewView
    android:id="@+id/preview_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

카메라 관련 동적 퍼미션 설정하기

퍼미션 설정하기

<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
  • <uses-feature android:name="android.hardware.camera.any" />
    카메라의 하드웨어적인 설정(플래쉬, 전면 카메라 사용 등) 을 할 수 있습니다. any 는 전면, 후면 카메라를 모두 사용하겠다고 하는 설정입니다.

  • <uses-permission android:name="android.permission.CAMERA"/>
    카메라 사용에 관한 퍼미션이며 동적 퍼미션 처리를 해주어야 합니다.

  • <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    레코드 오디오 관련 퍼미션입니다. 역시 동적 퍼미션 처리를 해주어야 합니다.

  • <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
    API 28버전 이하라면, 외부 저장소 사용에 관한 퍼미션이 필요합니다. 역시 동적 퍼미션 처리를 해주어야 합니다.

동적 퍼미션 처리하기

메니페스트에 퍼미션을 추가해주었으니, 동적 퍼미션 관련 처리를 해줍시다.

@Override
protected void onCreate(Bundle savedInstanceState) {
... 중략

 	ArrayList<String> permissions = new ArrayList<>();
    permissions.add(Manifest.permission.CAMERA);
    permissions.add(Manifest.permission.RECORD_AUDIO);
    if(Build.VERSION.SDK_INT <= 28) permissions.add(Manifest.permission.MANAGE_EXTERNAL_STORAGE);

    int checkResult = checkSelfPermission(permissions.get(0));
    int checkResult2 = checkSelfPermission(permissions.get(1));

    if(checkResult == PackageManager.PERMISSION_DENIED || checkResult2 == PackageManager.PERMISSION_DENIED){
        String[] arr = new String[permissions.size()];
        permissions.toArray(arr);
        resultLauncher.launch(arr);
    }
}

ActivityResultLauncher<String[]> resultLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<Map<String, Boolean>>() {
    @Override
    public void onActivityResult(Map<String, Boolean> result) {
        Set<String> keys = result.keySet(); 
        for(String key : keys){
            boolean value = result.get(key);
            if(value) Toast.makeText(MainActivity.this, key + " 퍼미션이 허용되었음", Toast.LENGTH_SHORT).show();
            else Toast.makeText(MainActivity.this, key + " 퍼미션이 거부되었음", Toast.LENGTH_SHORT).show();
        }
    }
});
  • ArrayList<String> permissions = new ArrayList<>();
    동적 퍼미션을 처리해야할 퍼미션들이 여러개 이므로, ArrayList 를 사용하여 처리해봅시다. 퍼미션은 문자열이므로, 제네릭 타입을 String 으로 지정합시다.

  • permissions.add(Manifest.permission.CAMERA);
    ArrayList 에 퍼미션을 추가해줍니다. 오디오와 관련한 퍼미션과 28버전 이하일 때 허용해주어야 하는 외부 저장소에 관한 퍼미션도 추가해줍니다. 단, 이번 테스트에서는 오디오와 카메라에 관한 퍼미션만 동적 퍼미션 처리를 해주도록 하겠습니다.

  • int checkResult = checkSelfPermission(permissions.get(0));
    checkSelfPermission 을 이용하여 ArrayList 의 0번째에 담긴 카메라 관련 퍼미션의 허용결과값을 checkResult 에 담아둡시다. 오디오 역시 마찬가지로 checkResult2 에 담아둡시다.

  • resultLauncher.launch(arr);
    만일, 오디오와 카메라의 퍼미션이 허용되지 않았다면, 허용을 해주는 다이얼로그를 사용자에게 띄워야 하므로, 런처를 이용해야합니다. 그리고 허용해야 하는 퍼미션을 launch() 의 파라미터로 전달해주면됩니다만, 파라미터로는 String[] 만 올 수 있기 때문에 ArrayList 를 String[] 으로 바꿔주어야 합니다.

  • Set<String> keys = result.keySet();
    런처가 요청값을 받아오면 Map<String, Boolean> 방식으로 받아오게 됩니다. String 은 퍼미션의 이름, Boolean 으로 허용결과를 가져옵니다. 우리는 받아온 결과값을 토대로 foreach 문으로 토스트를 띄워줄겁니다. 다만, Map 으로는 foreach 문을 사용할 수 없으므로 Set 형태로 키 값만 저장을 시켜 토스트를 띄워줍시다.

프리뷰 기능 구현하기

프리뷰란 우리가 카메라를 사용할 때 어떤 화면을 캡처할지 미리 볼 수 있는 기능입니다. 그 기능을 구현해봅시다.

void startCamera(){
    ListenableFuture<ProcessCameraProvider> listenableFuture = ProcessCameraProvider.getInstance(this);
    listenableFuture.addListener(new Runnable() {
        @Override
        public void run() {
            try {
                ProcessCameraProvider cameraProvider = listenableFuture.get();
                CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
                Preview.Builder builder = new Preview.Builder();
                Preview preview = builder.build();

                preview.setSurfaceProvider(previewView.getSurfaceProvider()); cameraProvider.bindToLifecycle(MainActivity.this,cameraSelector,preview);
            } catch (ExecutionException e) {
                throw new RuntimeException(e);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }, ContextCompat.getMainExecutor(this));
}

@Override
protected void onResume() {
    super.onResume();
    startCamera();
}
  • void startCamera()
    startCamera() 메소드는 프리뷰 기능을 시작하는 작업 메소드 입니다. 그리고 이 메소드는 라이프사이클 메소드인 onResume() 에서 호출하여, MainActivity 의 화면이 모두 보여진 후 프리뷰를 뿌려줍시다.

  • listenableFuture.addListener(new Runnable() {}, ContextCompat.getMainExecutor(this));
    ListenableFuture 의 기능인 addListener 를 이용하여 ProcessCameraProvider 을 얻어와야 합니다. 단, 비동기적인 방식으로 얻어와야 하기 때문에 파라미터로 Runnable 과 Main Thread 의 실행자가 필요합니다. 또한 ListenableFuture 객체는 ListenableFuture<ProcessCameraProvider> listenableFuture = ProcessCameraProvider.getInstance(this); 을 이용하여 가져올 수 있습니다.

  • ProcessCameraProvider cameraProvider = listenableFuture.get();
    ProcessCameraProvider 의 객체를 만들어줍시다. listenableFuture.get(); 을 이용하면 객체가 반환됩니다.

  • CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
    CameraSelector 객체 하나를 만들어주었습니다. 어떠한 종류의 카메라를 사용할지를 지정합니다. 이번 테스트에서는 후면 카메라로 지정해주었습니다.

  • Preview.Builder builder = new Preview.Builder();
    프리뷰 작업을 해줄 객체를 하나 만들어주어야 합니다. Builder 클래스를 이용하여 빌더 객체하나를 생성하고, 빌더 객체를 이용하여 Preview preview = builder.build(); 을 해주어 Preview 객체를 만들어 줍시다.

  • preview.setSurfaceProvider(previewView.getSurfaceProvider());
    프리뷰 작업을 해주는 프리뷰 객체에게는 SurfaceView 를 제공해주는 Provider 가 필요합니다. 다행히도, PreviewView 객체가 그 SurfaceProvider 를 가지고 있습니다. preview.setSurfaceProvider() 의 파라미터로 SurfaceProvider 를 제공해주면 됩니다.

  • cameraProvider.bindToLifecycle(MainActivity.this,cameraSelector,preview,imageCapture);
    앞서 만들어 두었던 cameraProvider 에게 어떤 라이프사이클과 연결시킬지 정합니다. 우리는 MainActivity 와 프리뷰를 연결시킬 예정이므로, 첫번째 파라미터에는 MainActivity.this 를 전달합니다. 두번째는 아까 만들어두었던 CameraSelector 를, 세번째에는 프리뷰 객체를 전달해주면 됩니다.

여기까지 하고, 이 메소드를 호출하면 프리뷰 기능 구현이 완성됩니다.

캡처 기능 구현하기

이제 프리뷰 기능은 구현해보았으니, 캡처 기능을 구현해봅시다. 먼저 화면에 FloatingActionButton 을 이용하여 캡처 버튼을 하나 만들어줍시다. 그리고 버튼을 누르면 clickBtn() 이 호출되도록 합시다. 우리는 clickBtn() 의 내부 구조만 살펴보겠습니다.

ImageCapture imageCapture = null;

void clickBtn(){
    if(imageCapture == null) return;

    SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss", Locale.KOREA);
    String fileName = sdf.format(System.currentTimeMillis());

    ContentValues values = new ContentValues();
    values.put(MediaStore.MediaColumns.DISPLAY_NAME,fileName);
    values.put(MediaStore.MediaColumns.MIME_TYPE,"image/jpeg");
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) values.put(MediaStore.MediaColumns.RELATIVE_PATH,"Pictures/CameraX-Image");

    ImageCapture.OutputFileOptions options = new ImageCapture.OutputFileOptions.Builder(getContentResolver(),MediaStore.Images.Media.EXTERNAL_CONTENT_URI,values).build();

    imageCapture.takePicture(options, ContextCompat.getMainExecutor(this), new ImageCapture.OnImageSavedCallback() {
        @Override
        public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
            Toast.makeText(MainActivity.this, "촬영 성공", Toast.LENGTH_SHORT).show();
            tv.setText(outputFileResults.getSavedUri().toString());
            Glide.with(MainActivity.this).load(outputFileResults.getSavedUri()).into(civ);
        }
        @Override
        public void onError(@NonNull ImageCaptureException exception) {
            Toast.makeText(MainActivity.this, "Error: " + exception, Toast.LENGTH_SHORT).show();
        }
    });
}
  • ImageCapture imageCapture = null;
    ImageCapture 객체의 참조변수를 하나 만들어 둡시다. 또한, ImageCapture 의 객체는 프리뷰 기능이 실행될 때, imageCapture = new ImageCapture.Builder().build(); 으로 만들어둡시다. 즉, startCamera() 내부에서 앞의 코드를 작성해주면 됩니다.

  • if(imageCapture == null) return;
    만일, imageCapture 가 생성되지 않았다면, 프리뷰가 제대로 생성되지 않았거나, 일련의 오류이기 때문에 일단 캡처의 실행은 중단합니다.

  • String fileName = sdf.format(System.currentTimeMillis());
    사진을 찍고 저장할 때 사용할 파일명을 만들어둡시다. 파일명은 중복이 되면 안되므로 날짜와 시간을 이용하여 작성해주어야 하며, SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss", Locale.KOREA); 을 이용하면, 어떠한 포맷으로 저장할지 결정할 수 있습니다.

  • ContentValues values = new ContentValues();
    CameraX 로 사진 촬영을 하고 저장을 할 때, CameraX 의 DB 로 저장이 되는데 이때의 record 의 정보 객체가 ContentValues 의 객체입니다. 이 객체에 put 기능을 이용하여 사진파일의 이름, 저장형식등을 설정해줄 수 있습니다. 또한 안드로이드의 버전이 P 버전 이상이라면, RELATIVE_PATH 의 속성 또한 지정해주어야 하며, 속성값은 "Pictures/CameraX-Image" 으로 지정되어있습니다.

  • ImageCapture.OutputFileOptions options = new ImageCapture.OutputFileOptions.Builder(getContentResolver(),MediaStore.Images.Media.EXTERNAL_CONTENT_URI,values).build();
    우리가 찍은 사진을 저장할 때 어떠한 옵션을 줄지 결정할 수 있으며, ImageCapture 객체에게 저장 옵션을 설정하기 위한 옵션 객체를 생성해주어야 합니다. Resolver 객체하나와, 외부저장소에 저장한다는 설정하나, 앞서 record 객체 하나 를 전달해주면 됩니다.

  • imageCapture.takePicture()
    이미지 캡처에게 takePicture() 를 요청하면 사진을 찍을 수 있으며, 파라미터로 옵션객체 하나와, 메인스레드의 실행자 하나, 성공했을때의 콜백메서드 onImageSaved() 와 실패했을 때의 콜백메서드 onError() 를 구현해주어야 합니다. 이번 테스트에서는 간단하게 토스트 메시지를 띄웠습니다.


VideoView

이번에는 동영상을 재생해줄 수 있는 VideoView 에 대해 알아봅시다. 크게 어렵지 않으니 간단하게 보고 갑시다.

<VideoView
    android:id="@+id/vv"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

xml 에 <VideoView> 를 하나 만들어줍시다. 동영상이 재생될 뷰 입니다.

String videoUrl = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    vv = findViewById(R.id.vv);

    vv.setVideoURI(Uri.parse(videoUrl));

    vv.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
        @Override
        public void onPrepared(MediaPlayer mediaPlayer) {
            vv.start();
        }
    });
}

실제 비디오 파일은 용량이 크기 때문에 res 폴더에 직접 가지고 있는 경우는 거의 없습니다. 보통은 웹서버에 동영상을 업로드 해두고 이를 불러와서 재생시키는 구조입니다. 테스트 목적이기 때문에 아무 영상의 url 을 가져왔습니다. 또한 인터넷을 사용해야 하므로 메니페스트 파일에 인터넷 퍼미션을 추가해줍시다.

vv.setVideoURI(Uri.parse(videoUrl)); 을 이용하여 받아온 url 을 비디오뷰에 set 해주었습니다. 그리고 나서 바로 vv.start() 로 비디오 재생을 시작하면 제대로 동작하지 않을 가능성이 있습니다. setVideoURI() 별도의 스레드를 이용하여 네트워크 작업을 하기 때문입니다. 따라서 우리는, 로딩이 모두 완료되었을때 비디오를 재생시켜주어야 합니다. 로딩작업에 대한 리스너 하나를 설정해주었으며, 로딩이 모두 끝나면, onPrepared() 콜백메서드가 실행됩니다. 그 안쪽에 vv.start() 를 호출해주면 됩니다.

그런데 막상 실행해놓고 보니 영상은 잘 실행되지만, 영상을 조작하기 위한 컨트롤러가 없습니다. vv.setMediaController(new MediaController(this)); 을 이용하여 컨트롤바를 붙여봅시다. 그러면 컨트롤바가 잘 붙은 모습을 볼 수 있습니다.


ExoPlayer

VideoView 보다 기능이 개선된 외부라이브러리 입니다.
ExoPlayer

ExoPlayer 사용하기

사용법을 알아봅시다. 일단 Uri videoUri = Uri.parse("http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"); 을 테스트용 영상으로 사용합시다.

먼저, 영상을 재생시켜줄 뷰 하나를 xml 에 정의합시다.

<com.google.android.exoplayer2.ui.StyledPlayerView
    android:id="@+id/pv"
    android:layout_width="match_parent"
    android:layout_height="300dp"
    android:layout_alignParentBottom="true"/>

이제 ExoPlayer 를 사용하러 가봅시다.

StyledPlayerView pv;
ExoPlayer exoPlayer;
MediaItem mediaItem;

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    pv = findViewById(R.id.pv);
    exoPlayer = new ExoPlayer.Builder(this).build();
    pv.setPlayer(exoPlayer);

    mediaItem = MediaItem.fromUri(videoUri);
    exoPlayer.setMediaItem(mediaItem);
    exoPlayer.prepare();
    exoPlayer.play();

    exoPlayer.setRepeatMode(ExoPlayer.REPEAT_MODE_ALL);
}
  • exoPlayer = new ExoPlayer.Builder(this).build();
    ExoPlayer 의 객체를 빌더 객체에게 만들어달라고 요청합시다.

  • pv.setPlayer(exoPlayer);
    영상을 재생할 뷰에 ExoPlayer 를 달아줍시다.

  • mediaItem = MediaItem.fromUri(videoUri);
    우리가 가져온 영상 Uri 를 MediaItem 의 객체에게 전달합시다. 그리고 mediaItem 을 이용하여 setMediaItem 으로 exoPlayer 에게 아이템을 전달합시다. 그러면 이제 exoPlayer.prepare(); 까지 해주면 영상을 재생할 준비는 끝났습니다.

  • exoPlayer.play();
    앞선 VideoView 와 다르게 따로 로딩완료를 듣는 리스너는 필요없습니다. 로딩완료까지 기다렸다가 재생해줍니다.

  • exoPlayer.setRepeatMode(ExoPlayer.REPEAT_MODE_ALL);
    재생의 종류를 지정합니다. REPEAT_MODE_ALL 은 모든 동영상을 반복 재생합니다.

컨트롤 박스 모양 지정하기

컨트롤 박스 모양을 커스텀하기 위해서는 별도의 레이아웃이 필요합니다. 레이아웃은 layout 폴더에 새로 만들어주어야 하는데, 이름이 지정이 되어있습니다. exo_player_control_view.xml 입니다. 이 파일 내부에서 컨트롤 박스의 모양을 커스텀 해줄 수 있습니다.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageButton
        android:id="@+id/exo_play"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        style="@style/ExoMediaButton.Play"
        android:background="#80000000"
        android:layout_centerInParent="true"/>
    <ImageButton
        android:id="@+id/exo_pause"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        style="@style/ExoMediaButton.Pause"
        android:background="#80000000"
        android:layout_centerInParent="true"/>
</RelativeLayout>

전체화면 기능 만들기

버튼을 누르면 전체화면으로 바뀌는 기능을 구현해봅시다. 전체화면은 새로운 화면을 구성하는것이므로, 새로운 액티비티를 활용해주어야 합니다. 즉, Intent 객체를 이용하여 새로운 액티비티로 넘어가도록 구현합시다. 또한, 영상 재생중에 전체화면으로 전환되므로 영상의 재생 위치의 정보 또한 Intent 에게 담아서 전체화면 액티비티를 실행시켜주어야 합니다.

먼저 MainActivity 에서 버튼을 눌렀을 때의 코드입니다.

Intent intent;
void clickBtn(){
    intent = new Intent(this, FullScreenActivity.class);

    intent.setData(videoUri);

    long currentPos = exoPlayer.getCurrentPosition();
    intent.putExtra("currentPos",currentPos);
    launcher.launch(intent);
}
Uri resultUri = null;
ActivityResultLauncher<Intent> launcher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),result -> {
    resultUri = intent.getData();
});

버튼을 눌러 전체화면을 만들어야 하며, Intent 를 이용해 영상의 재생 위치 정보를 넘겨주어야 합니다. 반대로, 전체화면에서의 영상 재생 정보 위치를 돌려받아야 하기 때문에 런처를 이용해야 합니다.

이번에는 activity_full_screen.xml 에서 전체화면의 구성을 짜봅시다.

<com.google.android.exoplayer2.ui.StyledPlayerView
    android:id="@+id/pv"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_alignParentBottom="true"
    app:resize_mode="fill"/>

전체화면일때 영상을 재생시켜줄 StyledPlayerView 하나를 설정해두었습니다. 그런데, 보통 전체화면은 가로모드로 바뀌어 재생되니 그 설정을 해주기 위해 메니페스트 파일로 갑시다.

<activity
    android:name=".FullScreenActivity"
    android:exported="false"
    android:screenOrientation="sensorLandscape"
    android:theme="@style/aa"/>

메니페스트 파일에서 FullScreenActivity 에 관련한 속성을 제어해주어야 합니다. android:screenOrientation="sensorLandscape" 속성을 이용하면 화면 모드를 설정해줄수있으며, 가로모드로 하기 위해 Landscape 로 설정합니다. 앞에 sensor 를 붙여주면 윗쪽과 아랫쪽이 따로 고정되있지 않으며 사용자가 디바이스를 들고있을 때 위쪽이 위로 설정됩니다.

이제 FullScreenActivity 에서의 코드를 봅시다.

public class FullScreenActivity extends AppCompatActivity {

    StyledPlayerView pv;
    ExoPlayer exoPlayer;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_full_screen);

        pv = findViewById(R.id.pv);
        exoPlayer = new ExoPlayer.Builder(this).build();
        pv.setPlayer(exoPlayer);

        Intent intent = getIntent();
        Uri vedioUri = intent.getData();
        long currentPos = intent.getLongExtra("currentPos",0l);
        
        MediaItem mediaItem = MediaItem.fromUri(vedioUri);
        exoPlayer.setMediaItem(mediaItem,currentPos);
        exoPlayer.prepare();
        exoPlayer.play();
    }
}
  • Intent intent = getIntent();
    MainActivity 로 부터 받아온 Intent 객체를 꺼내줍시다. 이 인텐트에는 영상에 관련한 정보와 영상 재생 위치에 관한 정보가 들어있습니다.

  • long currentPos = intent.getLongExtra("currentPos",0l);
    영상의 재생 위치 정보를 currentPos에 담아둡시다.

이제 Exoplayer 의 객체를 이용하여 영상을 재생시켜주기만 하면됩니다. 단, setMediaItem
() 을 호출할때 , currentPos 를 넘겨주어야 보던 부분부터 다시 재생시킬 수 있습니다.

profile
Developer

0개의 댓글