안드로이드 코틀린 : 동영상 촬영 후 외부저장소(앱 디렉토리)에 저장하고 미리보기 with FileProvider
Lucy Archive
Lucy / Facilitate4U
2021. 5. 4. 00:29

Android Kotlin : 기본 카메라 앱 호출해서 동영상 촬영, 저장, 미리보기

Intent의 암시적 호출 방법으로 카메라 앱으로 촬영한 동영상을 미리보기 및 저장하는 방법

입니다. 비슷한 방법으로 촬영할 영상을 내부 저장소 및 공용 저장소에 저장 할 수 있습니다. 필요하시면 하단의 링크를 참조해주세요.

 

프로젝트 생성 및 환경설정

프로젝트 생성

아래와 같이 프로젝트를 생성합니다.

  • 프로젝트명 : RecodeVideo
  • 사용언어 : Kotlin

※ 이 포스트는 Android Studio 4.1.2, Kotlin 1.4.31 버전으로 작성하였습니다.

 

Gradle 설정 : ViewBinding

App > Gradle Scripts > Build.gradle에 아래의 ViewBinding 설정 코드를 추가 후 Sync Now를 클릭하여 Gradle 변경 사항을 적용합니다.

android { 
	... 
    viewBinding { 
    	enabled = true 
    } 
}

Gragle 수정 사항 적용

※ ViewBInding은 Layout(xml)에 있는 레이아웃과, View의 Id를 코틀린 코드에서 직접 접근 할 수 있도록 해주는 도구 입니다. ViewBinding과 관련된 설명은 아래의 링크를 참조해주세요.

안드로이드 View Binding 사용하기

 

안드로이드 View Binding 사용하기 - kotlin-android-extensions 지원 중단

안드로이드 View Binding 방법 정리 안드로이드 코드에서 레이아웃 View에 접근하기 위해 사용된 kotlin-android-extensions 의 지원이 중단예정으로, 이를 대체하여 사용 할 수 있는 ViewBinding 사용법에 대해

juahnpop.tistory.com

 

(Manifest) 권한 및 FileProvider 설정

촬영한 동영상을 저장하기 위해 아래의 권한이 필요합니다. 권한 코드는 아래 코드에서 7~9줄에 해당됩니다.

  • 카메라 권한
  • 저장소 쓰기/저장

본 포스트는 제작된 앱에서 카메라 앱을 호출하여 지정된 URI에 파일을 저장하는 방식을 사용합니다. FileProvider는 지정된 파일의 콘텐츠 URI를 생성하는 기능을 가지고 있습니다. 자세한 내용은 안드로이드 개발자 가이드를 참고해주세요. FileProvider 설정 추가 코드는 16~24번 줄에 해당됩니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.blacklog.recordvideo">

    ...
    <!--Permissions -->
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    

    <application>
        
        ...
        <!--File Provider Start-->
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.blacklog.takepicture.fileprovider"
            android:grantUriPermissions="true"
            android:exported="false">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/filepaths" />
        </provider>
        <!--File Provider End-->
        ...
        
    </application>

</manifest>

위 코드를 작성하면 @xml/filepaths에 붉은 색이 표시되는데, 참조할 리소스 파일이 생성되지 않았기 때문입니다. 아래와 같이 app > res > xml 폴더 생성 후 filepaths.xml 파일을 추가합니다.

리소스 폴더 및 filepaths.xml 파일 추가

생성된 filepaths.xml 파일에 아래와 같이 코드를 작성합니다. 

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-files-path name="my_external_video" path="video/" />
    <root-path name="root" path="./"/>
</paths>

 

레이아웃 작성

activity_main.xml 코드 작성

아래와 같이 activity_main.xml 코드를 작성합니다. 각 뷰의 기능은 아래와 같습니다.

  • VideoView : 촬영한 비디오를 볼 수 있는 뷰
  • Button(ID : btnRecord) : 동영상 촬영 할 수 있는 앱을 호출
  • Button(ID : btnPlay)
    • 동영상을 재생 또는 정지하는 버튼
    • 디폴트 비활성화로 설정
    • 동영상 촬영이 정상적으로 완료되면 버튼 활성화
<?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">

    <Button
        android:id="@+id/btnPlay"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Play / Stop"
        android:layout_margin="10dp"
        android:enabled="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/btnRecord"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/btnRecord"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Record"
        android:layout_margin="10dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/btnPlay" />

    <VideoView
        android:id="@+id/videoView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_margin="10dp"
        app:layout_constraintBottom_toTopOf="@+id/btnRecord"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

MainActivity.kt 코드 작성

권한 코드 작성

Manifest에 설정한 권한을 요청하는 코드를 작성합니다. 예제용으로 간단하게만 구현하였습니다. 권한 설정 코드에 대한 설명은 아래의 링크를 확인해주세요.

안드로이드 런타임 권한 요청하기

 

안드로이드 코틀린 : 런타임 권한(위험 권한) 요청 코드 작성 방법 - 카메라 및 외부 저장소 권한

안드로이드 권한2 : 위험 권한 허가 확인 및 허가 요청 코드 작성 방법 이전 포스트에서 안드로이드 권한에 대해 간략히 설명하고 일반 권한 사용법을 정리하였습니다. 이번 포스트에서는 카메

juahnpop.tistory.com

package com.blacklog.cameraxvideo

import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.blacklog.cameraxvideo.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    lateinit var binding : ActivityMainBinding

    private val CAMERA_PERMISSION = arrayOf(Manifest.permission.CAMERA)
    private val CAMERA_PERMISSION_FLAG = 100
    private val STORAGE_PERMISSION = arrayOf(
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE)
    private val STORAGE_PERMISSION_FLAG = 200

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        if(checkPermission(CAMERA_PERMISSION, CAMERA_PERMISSION_FLAG)){
            checkPermission(STORAGE_PERMISSION, STORAGE_PERMISSION_FLAG)
        }
    }

    private fun checkPermission(permissions : Array<out String>, flag : Int):Boolean{
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            for (permission in permissions) {
                if (ContextCompat.checkSelfPermission(
                                this,
                                permission
                        ) != PackageManager.PERMISSION_GRANTED
                ) {
                    ActivityCompat.requestPermissions(this, permissions, flag)
                    return false
                }
            }
        }
        return true
    }

    override fun onRequestPermissionsResult(
            requestCode: Int,
            permissions: Array<out String>,
            grantResults: IntArray
    ) {
    	when(requestCode){
            CAMERA_PERMISSION_FLAG -> {
                for(grant in grantResults) {
                    if(grant != PackageManager.PERMISSION_GRANTED){
                        Toast.makeText(this, "카메라 권한을 승인해야지만 앱을 사용 할 수 있습니다.", Toast.LENGTH_LONG).show()
                        finish()
                    }else{
                        checkPermission(STORAGE_PERMISSION, STORAGE_PERMISSION_FLAG)
                    }
                }
            }
            STORAGE_PERMISSION_FLAG -> {
                for(grant in grantResults) {
                    if(grant != PackageManager.PERMISSION_GRANTED){
                        Toast.makeText(this, "저장소 권한을 승인해야지만 앱을 사용 할 수 있습니다.", Toast.LENGTH_LONG).show()
                        finish()
                    }
                }
            }
        }
    }
}

위 코드를 작성 후 실행하면 앱 실행시 아래와 같이 앱 권한 요청 다이얼로그가 표시됩니다. 

권한 설정

동영상 촬영 및 저장 코드 작성하기

위 UI에서 RECORD 버튼을 누르면 암시적 인텐트를 사용하여 동영상 앱을 호출합니다. 인텐트 인스턴스에는 동영상 파일이 저장될 URI를 포함합니다. 현재 앱 디렉토리의 파일을 카메라 앱으로 파일을 안전하게 전송하기 위해 FileProvider를 사용하여 파일을 Content Uri로 변환하여 카메라 앱에서 전송함과 동시에 파일을 사용할 수 있도록 권한을 부여합니다. 아래는 관련 코드만 표시하였습니다.

class MainActivity : AppCompatActivity() {

    ...

    private val REQUEST_VIDEO_CAPTURE_CODE = 1
    private var videoUri : Uri? = null // video 저장될 Uri

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        ...

        binding.btnRecord.setOnClickListener {
            val recordVideoIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
            val videoFile = File(
                    File("${filesDir}/video").apply {
                        if(!this.exists()){
                            this.mkdirs()
                        }
                    },
                    newVideoFileName()
            )
            videoUri = FileProvider.getUriForFile(
                    this,
                    "com.blacklog.cameraxvideo.fileprovider",
                    videoFile
            )
            recordVideoIntent.resolveActivity(packageManager)?.also{
                recordVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, videoUri)
                startActivityForResult(recordVideoIntent, REQUEST_VIDEO_CAPTURE_CODE)
            }
        }
       
        ...
    }
    
    ....
    
    private fun newVideoFileName() : String {
        val sdf = SimpleDateFormat("yyyyMMdd_HHmmss")
        val filename = sdf.format(System.currentTimeMillis())
        return "${filename}.mp4"
    }
    
    ....
}

카메라 앱에서 동영상 촬영이 정상적으로 완료되면 VideoView와 저장된 동영상 파일을 연결하고, Play/Stop 버튼을 활성화 합니다. onActivityResult는 코드창에서 Ctrl + O 를 눌러 검색 후 override 할 수 있습니다.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if(resultCode == RESULT_OK){
        when(requestCode){
            REQUEST_VIDEO_CAPTURE_CODE -> {
                binding.videoView.setVideoURI(videoUri)
                binding.videoView.requestFocus()
                binding.btnPlay.isEnabled = true
            }
        }
    }
}

마지막으로, Play/Stop 버튼 동작을 처리하는 코드를 추가합니다. VideoView에 동영상이 재생되고 있는지를 확인하는 Boolean타입의 변수 isVideoPlaying을 전역 변수로 선언합니다. Play / Stop 버튼 동작을 눌렀을 떄 isVideoPlaying 의 상태에 따라 영상을 재생하거나, 종료합니다. videoView.setOnCompletionListener 는 VideoView의 영상이 재생 완료되었을 때 실행되는 코드입니다. 영상 재생이 완료되면 버튼의 텍스트를 변경하고, isVideoPlaying 의 값을 업데이트 합니다.

class MainActivity : AppCompatActivity() {

    ...
    
    private var isVideoPlaying = false // VideoView에 영상이 재생되고 있는지 상태를 확인

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        ...

        binding.btnPlay.setOnClickListener {
            when(isVideoPlaying){
                true -> {
                    isVideoPlaying = false
                    binding.videoView.stopPlayback()
                    binding.btnPlay.text = "Play"
                    binding.videoView.setVideoURI(videoUri)

                }
                false -> {
                    isVideoPlaying = true
                    binding.btnPlay.text = "Stop"
                    binding.videoView.start()
                }
            }
        }

        binding.videoView.setOnCompletionListener {
            binding.btnPlay.text = "Play"
            isVideoPlaying = false
        }

    }
    ...
}

실행 결과

위 코드를 실행하면 아래와 같이 촬영한 영상을 VideoView에서 재생 할 수 있습니다. 촬영한 영상은 외장메모리의 앱 디렉토리에 저장되기 떄문에 겔러리앱에서 보이지 않습니다. 

실행 화면

추가로, 내부저장소 또는 공용저장소에 저장하는 방법이 필요하면 아래의 링크를 참조해주세요.

카메라 앱으로 찍은 사진 내부저장소, 외부저장소, 공용저장소 저장 방법

 

안드로이드 코틀린 : 카메라 사진 찍기, 내부저장소, 외부저장소, 공용저장소 저장 방법 with FilePr

Android Kotlin : 기본 카메라 앱 호출해서 사진 찍기 Intent의 암시적 호출 방법으로 카메라를 호출하여 찍은 사진의 미리보기 및 원본을 내부저장소, 외부저장소, 공용저장소의 특정 폴더에 저장하

juahnpop.tistory.com

끝까지 읽어 주셔서 감사합니다.^^

관련포스트

🤞 안드로이드(코틀린) 앱 제작하기 관련글 목록 보기

🤞 안드로이드 권한 관련글 목록 보기

🤞 안드로이드(코틀린) Intent 관련글 목록 보기

🤞 안드로이드(코틀린) 저장소 관련글 목록 보기

🤞 안드로이드(코틀린) FileProvider 관련글 목록 보기

🤞 안드로이드(코틀린) 카메라 관련글 목록 보기