Building a Video to GIF Android App Using Kotlin and Cloudinary
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.
We will select API 16 as our minimum SDK. The minimum SDK is the lowest Android version that the app supports.
Thereafter, you select Empty Activity, enter your activity name and click finish. This basic setup is sufficient for our implementation.
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. UsingstartActivityForResult
means, we expect a response from the video selection process. This response is rendered in theonActivityResult
function. The process can either be successful (if a video was selected) or unsuccessful (the operation was cancelled). -
onActivityResult
: If a video was selected, theresultCode
will be equal toActivity.RESULT_OK
and if it was cancelled, theresultCode
will be equal toActivity.RESULT_CANCELLED
. So in this function, we check whether the request code matches our earlier sent request code and that the result code equalsActivity.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.
Great post you got here!