[Android] 서버에 동영상 전송하기 :multipartFile, API 서버 연동

API 서버에 내용 + 비디오 전송하기 

 

이전에 서버로 이미지를 전송하는 포스팅을 작성하였으나 비디오 전송은

다른 방식을 필요로 해서 한번 더 기록하게 되었다.

프로덕트에 대한 동영상 리뷰가 필요한데, multipart/form-data 형식으로 전송해야 한다.

 

 

 

ViewModelScope

viewModelScope는 viewModel 내부에서 사용되며,

lifecycle을 인식하는 코루틴 스코프를 만들 수 있다.

이 방식은 viewModelScope 블럭에서 실행되는 작업들을

별도의 처리 없이  ViewModel이 clear 되는 순간 자동으로 취소하게 할 수 있다.

데이터를 전송하기 위해 multipart로 데이터를 변환하는 작업은 취소하게 되면 작업 취소 과정을 생략할 수 있다.

 

새로 데이터를 추가하는 함수를 ViewModel 내부에 선언하고, 해당 함수에 viewModelScope를 적용한다.

try 구문 내에서 데이터 변환과 전송을 시도한다.

 

새로 데이터를 추가하는 함수를 ViewModel 내부에 선언하고, 해당 함수에 viewModelScope를 적용한다.

try 구문 내에서 데이터 변환과 전송을 시도한다.

아래와 같은 형식을 가지고 있다.

 

class AddReviewViewModel : ViewModel(){
    @RequiresApi(Build.VERSION_CODES.Q)
    fun uploadReviewPoster(reviewText: String, filePath: Uri?,
                        activity: Activity, paramFunc:(ReviewData?,String?)->Unit){
        viewModelScope.launch {
            try {
                        }catch (e:java.lang.Exception){
                paramFunc(null,"error")
                Log.d("error post ",e.toString())
            }

 

try 구문에서 데이터 변환과 서버로 전송이 이루어진다.

 

우선 기기의 동영상을 가져오기 위해서 권한 설정과 요청을 한다.

 

// 권한 설정
readGalleryListener = object : PermissionListener{
    override fun onPermissionGranted() {
        getVideo()
    }

    // 권한 실패
    override fun onPermissionDenied(deniedPermissions: MutableList<String>?) {
        Toast.makeText(activity,"권한 필요",Toast.LENGTH_SHORT)
            .show()
        activity.returnView(this@UploadReviewFragment)
    }

}

// 갤러리 권한 요청
TedPermission.with(activity.applicationContext)
    .setPermissionListener(readGalleryListener)
    .setPermissions(
        android.Manifest.permission.READ_EXTERNAL_STORAGE,
        android.Manifest.permission.WRITE_EXTERNAL_STORAGE
    ).check()

 

권한이 허용된 경우 getVideo() 함수를 통해서 갤러리에 접근한다.

intent를 통해서 문서함을 열고, 동영상 파일을 필터링해서 가져온다.

//갤러리의 비디오 얻어오기
private fun getVideo(){
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
    intent.type = "video/*"
    activityResult.launch(intent)
    setButtonColor()
}

 

동영상을 문서에서 선택하고 다시 fragment로 돌아오면 해당 비디오의 uri를 받을 수 있다.

private val activityResult: ActivityResultLauncher<Intent> = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
) 

ActivityResultLauncher를 통해서 intent에서 전달 받은 값을 처리한다.

성공적인 응답값이 왔을 때 비디오 Uri를 처리하는 코드이다.

 

if (it.resultCode==RESULT_OK && it.data != null){
    videoUrl  = it.data!!.data
}

 

이제 위에서 제작한 viewmodel의 함수를 통해서 서버로 데이터를 전달한다.

addReviewModel.uploadReviewPoster

위 함수 실행

 

함수 내부에서는 데이터의 변환이 이루어져야한다.

내용 부분은 서버에서 원하는 parms 형식으로 데이터를 받아야하며,

동영상 부분은 서버에서 원하는 형식인 multpartfile 형식으로 전송해야 한다.

@Multipart
@POST("products/{productId}/reviews")
fun addReView(
    @Path("productId") productId: Int,
    @Part("param") param : BodyReviewForPOST, //body
    @Part multipartFile : MultipartBody.Part? //video
): Call<ReviewData>

 

내용 부분(param)은 형식만 맞춘 뒤에 서버로 전송하면 된다.

val reviewBody = ReviewBody(reviewText,addProductIdList)
val body = BodyReviewForPOST(reviewBody)

 

반면에 동영상(multpartfile) 부분은 데이터 변환이 필요하다.

우선 서버로 데이터를 전송하기 위해서 동영상 Uri를 File path(String) 형식으로 변환해야 한다.

이 때 getFilePath 함수를 활용한다.

fun getFilePath(activity: Activity, contentUri: Uri): String{
    val cursor  = activity.contentResolver.query(contentUri,
    null,null,null)

    cursor?.use {
        if (it.moveToFirst()){ // 첫 발견
            val displayName :String = it.getString(it
                .getColumnIndex(OpenableColumns.DISPLAY_NAME))
            val projection = arrayOf(Media.DATA)
            val newCursor = activity.contentResolver.query(Media.EXTERNAL_CONTENT_URI
            ,projection,null,null,null)
            newCursor?.use {it2->
                if (it2.moveToFirst()){
                    val data = it2.getString(it2.getColumnIndex(Media.DATA))
                    if (data.contains(displayName)){
                        return data
                    }
                }
            }
        }
    }

    return "null"
}

 

getFilePath()는 Document에서 가져온 파일을 파일 명을 통해서 탐색하는 함수이다.

  • 같은 파일 명을 가진 파일을 탐색 (image, vedio 둘 다 가능)
  • 커서 이동하면서 같은 파일명을 찾으면 바로 리턴
val multipartFile : MultipartBody.Part? = if (filePath!=null){
    val path = getFilePath(activity,filePath)
    val file = File(path)
    val videoRequestBody = file.asRequestBody("video/mp4".toMediaTypeOrNull())
    MultipartBody.Part.createFormData(
        "multipartFile",
        file.name,
        videoRequestBody
    )
}else{null}

 

해당 File Path를 얻었다면 데이터 변환을 시도한다.

만약에 전달받은 filePath가 없다면 null을 반환한다.

 

이 후에 레트로핏 함수를 통해서 데이터를 전달한다.

@Multipart
@POST("products/{productId}/reviews")
fun addReView(
    @Path("productId") productId: Int,
    @Part("param") param : BodyReviewForPOST, //body
    @Part multipartFile : MultipartBody.Part? //video
): Call<ReviewData>
((activity as AddReviewActivity).masterApp).service
    .addReView(productId,body,multipartFile)

 

서버에 성공적으로 동영상이 업로드 되었다.

m3n8 형식으로 동영상이 저장되는 이유는 모르겠으나, 

서버의 파일을 열어보니 동영상 파일이 성공적으로 저장된 것을 확인했다.