Codementor Events

Building a Video to GIF Android App Using Kotlin and Cloudinary

Published Dec 04, 2017Last updated Dec 10, 2017

​​Introduction

GIFs
Graphics Interchange Format - also know as GIF - is a bitmap image format. It is simply an image that has animations. GIFs usually represent a short animated image of people doing crazy, embarrassing or unusual things. They can be used to express emotions when we can’t do it in person. So many GIFs today have turned unknown people into online celebrities. Today, you won’t be left out of the action.

Cloudinary
Cloudinary is a cloud-based end-to-end image and video management solution that supports uploads, storage, manipulations, optimizations and delivery. Cloudinary helps you store images/videos and perform transformations, such as resizing and adding effects to the resource. With the help of Cloudinary, we will upload a video and convert it to a GIF with ease. 

​​Requirements

​​You need to own a Cloudinary account. You can quickly create one here.

​​At the time of this publication, Android Studio 3.0 is recommended to create your Android app, as it integrates Kotlin support with ease. Android studio is the official IDE for developing Android applications. You can checkout the latest Android studio versions here.

​​Setting Up Our Android Client

Creating project
​​Open Android Studio and create a new Android project. Insert the application name, package name and check the “Include Kotlin Support” checkbox. 

​​  User-uploaded image: cloudinary1.png

​​
​​We will select API 16 as our minimum SDK. The minimum SDK is the lowest Android version that the app supports. 

User-uploaded image: Cloudinary2.png

​​Thereafter, you select Empty Activity, enter your activity name and click finish. This basic setup is sufficient for our implementation.

​​  User-uploaded image: Cloudinary3.png

Installing Cloudinary
​​To be able to access Cloudinary’s features, we need to insert the dependency. It will be added in the project build.gradle file.

​​implementation group: 'com.cloudinary', name: 'cloudinary-android', version: '1.21.0'

​​Then you open the AndroidManifest.xml file,  and insert the configurations under the application tag:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"`
   package="com.example.android.cloudinarysample">`

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>`
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>`

    <application
        ...
        >
        <meta-data
            android:name="CLOUDINARY_URL"
            android:value="cloudinary://@myCloudName"/>
    </application>
</manifest>

​​Replace myCloudName with your personal Cloudinary name, which is found on your console. We also added the storage permissions in the manifest.

​​Next up, you create a class that extends the application class. In our case, we will name it AppController:

public class AppController : Application() {
    override fun onCreate() {
        super.onCreate()
        // Initialize Cloudinary
        MediaManager.init(this)
    }
}

This class helps us to initialize the MediaManager all during the app lifecycle. This initialization helps to setup the library with required parameters, such as the cloud name earlier injected in the AndroidManifest.xml file.

We also have to add the AppController class to the AndroidManifest as name of the application tag:

<application
    android:name=".AppController" >

App Layout
Here is the snippet for our layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center"
    android:layout_margin="16dp"
    android:gravity="center"
    android:orientation="vertical"
    tools:context="com.example.android.cloudinarysample.MainActivity">
    
    <ProgressBar
        android:id="@+id/progress_bar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="invisible" />

    <Button
        android:id="@+id/button_upload_video"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:text="Upload Video" />


</LinearLayout>

In our layout, we have a progress bar that will spin when we are either uploading or downloading. We also have a button to trigger a video selection from the phone storage.

Upload to Cloudinary

Cloudinary offers two types of uploads: signed and unsigned.

Signed uploads requires an authentication signature from a back-end. With signed uploads, your images and videos are signed with the API and secret key found in the console. Since using these keys are risky on a client side that can easily be decompiled, there is need for a backend.

Unsigned uploads on the other hand are less secure than signed uploads. They do not require any signature for uploads. Unsigned upload options are controlled by an upload preset. An upload preset is used to define options to be applied to images that are uploaded with the preset. Unsigned uploads, however, have other limitations. For example, existing images cannot be overwritten. The options you set in the unsigned preset also can limit the size or type of files that users can upload to your account.

In this demo, we will be implementing the unsigned upload. So, we need to enable unsigned uploads on our console. Select Settings on your dashboard, select the Upload tab, scroll down to where you have upload preset, and enable Unsigned Uploading. A new preset will be generated with a random string as its name. Copy it out as we will need it soon.

Next up, we will get a video from our phone storage, upload it to Cloudinary, perform a transformation on it and download the .gif file. We will implement all this in our MainAcivity.kt:

  private val SELECT_VIDEO: Int = 100
    lateinit var TAG:String
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        this.TAG = localClassName
        button_upload_video.setOnClickListener {
            if (checkStoragePermission()) {
                openMediaChooser()
            } else {
                requestPermission()
            }
        }
        
    }
    
    fun openMediaChooser() {
        val intent = Intent(Intent.ACTION_PICK, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
        startActivityForResult(intent, SELECT_VIDEO)
    }
    
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    
        if (requestCode == SELECT_VIDEO && resultCode == Activity.RESULT_OK) {
            progress_bar.visibility = VISIBLE
            MediaManager.get()
                    .upload(data!!.data)
                    .unsigned("YOUR_PRESET")
                    .option("resource_type", "video")
                    .callback(object : UploadCallback {
                        override fun onStart(requestId: String) {
                            Log.d(TAG, "onStart")
                        }
    
                        override fun onProgress(requestId: String, bytes: Long, totalBytes: Long) {
    
                        }
    
                        override fun onSuccess(requestId: String, resultData: Map<*, *>) {
                            Toast.makeText(this@MainActivity, "Upload successful", Toast.LENGTH_LONG).show()
                            
                        }
    
                        override fun onError(requestId: String, error: ErrorInfo) {
                            Log.d(TAG,error.description)
                            progress_bar.visibility = INVISIBLE
                            Toast.makeText(this@MainActivity,"Upload was not successful",Toast.LENGTH_LONG).show()
                        }
    
                        override fun onReschedule(requestId: String, error: ErrorInfo) {
                            Log.d(TAG, "onReschedule")
                        }
                    }).dispatch()
    
        }
    
    }

In the above snippet, we have three functions;

onCreate: This is called when the activity is created at the start of the app. Here we assign our earlier designed XML file as the default layout of the activity. We also added a listener to the button. When the button is selected, the app checks if the storage permissions have been granted. If it has, the second function openMediaChooser is called. If it was not, the app requests for storage permissions. If the request is successful, openMediaChooser is then called. The checkStoragePermission, requestPermission, and onPermissionsResult functions are omitted here for brevity, however, you can find get the snippets in the github repository.

  • openMediaChooser : This function opens the phone’s gallery for a video to be selected. This process has a unique request code stored in the variable SELECT_VIDEO. Using startActivityForResult means, we expect a response from the video selection process. This response is rendered in the onActivityResult function. The process can either be successful (if a video was selected) or unsuccessful (the operation was cancelled).

  • onActivityResult : If a video was selected, the resultCode will be equal to Activity.RESULT_OK and if it was cancelled, the resultCode will be equal to Activity.RESULT_CANCELLED. So in this function, we check whether the request code matches our earlier sent request code and that the result code equals Activity.RESULT_OK.

If a video was successfully chosen, we trigger an upload to Cloudinary. We do this by building
an UploadRequest and dispatching it. The UploadRequest takes different methods, such upload where we insert the URI of the video selected, unsigned where we insert the preset name earlier gotten from the console, option where you insert the resource type to be uploaded and the UploadCallback to track the progress of the upload.

Transformation

If the upload was successful, the onSuccess method is called. This method provides us with the requestId and details from the upload. We can access the URL of the just-uploaded video by calling resultData["url"]. Instead of a video, we need a .gif file. We can easily get this by changing the extension of the video to .gif. So, it goes this way:

val publicId:String = resultData["public_id"] as String
    val gifUrl: String = MediaManager.get()
            .url()
            .resourceType("video")
            .transformation(Transformation<Transformation<out Transformation<*>>?>().videoSampling("12")!!.width("174").height("232").effect("loop:2"))
    .generate("$publicId.gif")

Each uploaded video has a unique id from Cloudinary. This id can be accessed by calling resultData["public_url"]. So the resulting gifUrl value is a result of getting your Cloudinary URL based on your cloud name (usually something like this res.cloudinary.com/{cloud name}/), appended with the transformations, resource type to be accessed, its unique id stored on the cloud, and the output format(.gif in our case).

From the snippet, we added transformations to our GIF file: shrinking the height and width to 232 and 174 respectively, setting the frame rate to 12fps, and making the GIF loop twice. Without this transformation, we might have gotten a larger file than the uploaded video, which defeats the aim of GIFs. GIFs should be small in size. You can play around the transformations and learn more about them here.

Download the GIF file

Next up, we download the GIF file. We will use the PRDownloader library to aid this.

In our build.gradle file, we will add the download library dependency:

implementation 'com.mindorks.android:prdownloader:0.2.0'

In the AppController.kt, we initialize the PRDownloader library:

public class AppController : Application() {
        override fun onCreate() {
            super.onCreate()
            // Initialize Cloudinary
            MediaManager.init(this)
            // Initialize the PRDownload library
            PRDownloader.initialize(this)
        }
    }

We then create a function downloadGIF :

private fun downloadGIF(url: String, name: String) {
        val downloadId =
                PRDownloader
                        .download(url, getRootDirPath(), name).build()
                        .setOnStartOrResumeListener(object : OnStartOrResumeListener {
                            override fun onStartOrResume() {
                                Log.d(TAG,"download started")
                            }
                        })
                        .setOnPauseListener(object : OnPauseListener {
                            override fun onPause() {
    
                            }
                        })
                        .setOnCancelListener(object : OnCancelListener {
                            override fun onCancel() {
    
                            }
                        })
                        .setOnProgressListener(object : OnProgressListener {
                            override fun onProgress(progress: Progress) {
    
                            }
                        })
                        .start(object : OnDownloadListener {
                            override fun onDownloadComplete() {
                                progress_bar.visibility = INVISIBLE
                                Toast.makeText(this@MainActivity,"Download complete",Toast.LENGTH_LONG).show()
                            }
    
                            override fun onError(error: Error) {
                                Log.d(TAG,error.toString())
                            }
                        })
    
    }
    
    private fun getRootDirPath(): String {
        return if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
            val file = ContextCompat.getExternalFilesDirs(this@MainActivity,
                    null)[0]
            file.absolutePath
        } else {
            this@MainActivity.filesDir.absolutePath
        }
    }

This function handles the download of the GIF file. PRDownload.download takes in three parameters: the link of what is to be downloaded, the directory where it is to be stored and the name of the file.

This function will be called in the onSuccess function of the UploadCallback after the gifUrl has been generated:

override fun onSuccess(requestId: String, resultData: Map<*, *>) {
        ...
        downloadGIF(gifUrl,"$publicId.gif")
    }

The file will be downloaded to your phone storage in this directory Android/data/{app package name}/files.

Conclusion

Cloudinary offers a lot of APIs that ease the burden of image/video storage and manipulations. This demonstration is just the tip of the iceberg and you can explore more APIs here. With this post, we have learned how to convert our videos to GIFs. You can find the full source code here.

Discover and read more posts from Idorenyin Obong
get started
post commentsBe the first to share your opinion
Christian Nwamba
6 years ago

Great post you got here!

Show more replies