Android – Tạo ứng dụng Convert video sang GIF

2
Dịch vụ dạy kèm gia sư lập trình

Hôm nay mình sẽ hướng dẫn các bạn một cách dễ nhất để tạo một ứng dụng Android chuyển đổi video sang GIF.

Để tạo ứng dụng này, mình không sử dụng OpenCV vừa phức tạp, lại cồng kềnh. Đơn giản và nhanh nhất là sử dụng một dịch vụ có sẵn, có tên là Cloudinary.

Cụ thể Cloudinary là gì? Cách convert video sang gif sử dụng Cloudinary như thế nào? Việc tích hợp vào ứng dụng android có đơn giản không?

Chúng ta cùng theo dõi bài viết dưới đây nhé.

Giới thiệu tổng quan về ảnh ảnh động GIF

Trước khi chúng ta bắt tay vào xây dựng app tạo ảnh động cho android, mình muốn giới thiệu kiến thức nền tảng trước về ảnh gif.

#Ảnh GIF là gì?

GIF (viết tắt của Graphics Interchange Format) là một định dạng hình ảnh bitmap có khả năng chuyển động. Chúng ta hiểu đơn giản, ảnh gif là một tập hợp nhiều khung hình chạy liên tục.

Gọi là ảnh động vì GIF không phải là video, tất nhiên là không có âm thanh rồi. Tuy nhiên, khi lật lại vấn đề, nếu video mà bỏ âm thanh đi thì nó cũng có thể coi là ảnh gif.

Bới vậy, nhu cầu về một ứng dụng Android chuyển đổi video thành GIF là rất lớn. Cũng là một ý tưởng tạo ứng dụng không tồi phải không?

Rất nhiều ảnh GIF đã biến người bình thường thành người nổi tiếng trên mạng. Trên mạng không thiếu những ảnh gif rất độc đáo nếu bạn chịu khó search một chút.

#Dịch vụ lưu trữ ảnh/video Cloudinary

Cloudinary là một giải pháp quản lý video và hình ảnh từ đầu đến cuối dựa trên công nghệ đám mây. Cloudinary hỗ trợ lưu trữ, các thao tác, tối ưu và phân phối các file media.

Cloudinary giúp bạn lưu trữ hình ảnh/video và chỉnh sửa ở mức cơ bản như chỉnh kích thước, thêm các hiệu ứng.

Với sự trợ giúp của Cloudinary, bạn có thể convert video thành một ảnh GIF một cách dễ dàng, chỉ cần upload video lên và tận hưởng thành quả.

Cũng giống như các dịch vụ cloud như Firebase, Cloudinary có nhiều gói dịch vụ cả miễn phí và trả phí. Mới đầu bạn cứ dùng gói miễn phí, cũng xả láng cho bạn rồi.

Bảng giá dịch vụ lưu trữ ảnh đám mây cloudinary
Bảng giá dịch vụ lưu trữ ảnh đám mây cloudinary

Xây dựng app Android chuyển đổi video sang gif

#Công tác chuẩn bị

Để xây dựng ứng dụng như mình nói, chúng ta cần chuẩn bị trước 2 điều sau:

  • Một tài khoản Cloudinary. Nếu chưa bạn có thể đăng ký miễn phí tại đây.
  • Android Studio 3.0 hoặc mới hơn.
Android Studio là một IDE chính thức cho phát triển các ứng dụng Android. Bạn có tham khảo chi tiết cách cài đặt Android Studio tại đây.

#Tạo Android Project (Client side)

1. Tạo một android project mới

Đầu tiên để tạo ứng dụng Android chuyển đổi video thành ảnh gif, bạn hãy mở Android Studio và tạo một project Android mới.

Điền tên ứng dụng và package. Nhớ tick vào checkbox “Include Kotlin Support”. Mình sẽ code bằng ngôn ngữ Kotlin thay vì Java nhé (cho hợp với thời đại).

Hướng dẫn tạo ứng dụng Android chuyển đổi video thành GIF
Tạo mới một dự án Android trong Android Studio

Chúng ta đặt min SDK là level 16 (tức là Android 4.1).

Nhắc lại: Min SDK là phiên bản Android cũ nhất mà ứng dụng có thể chạy được.
Hướng dẫn tạo ứng dụng Android chuyển đổi video thành GIF
Đặt tên ứng dụng

Sau đó bạn chọn Empty Activity, nhập tên cho main Activity và nhấn finish.

Hướng dẫn tạo ứng dụng Android chuyển đổi video thành GIF
Chọn Empty Activity.

Như vậy là xong phần thiết lập cơ bản, chúng ta chuyển sang phần code nhé.

2. Cài đặt và tích hợp Cloudinary vào dự án android

Để sử dụng được các tính năng của Cloudinary, chúng ta cần chèn thêm thư viện (dependency).

Bạn thêm đoạn code sau vào tệp build.gradle:

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

Sau đó bạn mở file AndroidManifest.xml, và chèn các configurations dưới tag <application>

<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>

Đừng quên thay thế myCloudName bằng tên Cloudinary cá nhân của bạn nhé.

Ngoài ra ứng dụng cũng cần Storage Permission để đọc ghi vào bộ nhớ. Nên các bạn cũng cần phải add thêm android.permission.WRITE_EXTERNAL_STORAGEpermission vào manifest.

Cách yêu cầu cấp quyền runtime permission: Yêu cầu Runtime permission cho ứng dụng Android

Tiếp theo bạn tạo ra một AppController Class extend từ Application.

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

Class này giúp chúng ta khởi tạo một instance của MediaManager tồn tại trong suốt vòng đời của ứng dụng.

Sự khởi tạo này giúp thiết lập thư viện với tham số như tên cloud được chèn trong tập tin AndroidManifest trước đó .

Ngoài ra, chúng ta cũng phải bổ sung AppController vào AndroidManifest để làm tên của tag ứng dụng như bên dưới.

<application
    android:name=".AppController" >

3. Thiết kế Layout ứng dụng

Vì tạo layout chỉ đơn giản là kéo thả các UI nên mình sẽ không giải thích về phần này nhé. Kết quả là mình có một snippet cho layout ứng dụng mà chúng ta đang làm.

<?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>

Trong layout này, chúng ta có một thanh ProgressBar. Tác dụng là nó sẽ hiểu thị trong khi chúng ta upload video hoặc download ảnh gif.

Ngoài ra, chúng ta có một Button để chọn video từ bộ nhớ (local storage).

#Cấu hình Upload video trên Cloudinary (back-end side)

Để tạo app Android chuyển đổi video thành GIF thì ta cần đến Cloudinary hỗ trợ hai kiểu upload: signed và unsigned

1. Signed uploads yêu cầu một chữ ký xác thực phía back-end. Với loại upload này, hình ảnh và video của bạn sẽ được signed với API và secret key được tìm ở bảng console.

Vì việc sử dụng các key phía client side khá không an toàn (có thể dễ dàng bị dịch ngược) nên việc này sẽ thực hiện ở phía Back-end.

2. Unsigned uploads kém an toàn hơn so với Signed uploads. Ngoài ra, Unsigned uploads còn có những hạn chế khác.

Ví dụ như những hình ảnh hiện ra bị đè lên nhau. Mục option mà bạn đã thiết lập trên upload preset cũng bị giới hạn bởi kích thước hay loại tập tin mà người dùng upload lên tài khoản của bạ

Trong demo này, chúng ta sẽ thực hiện unsigned upload cho nó dễ (Còn bạn nào muốn hiểu và code nâng cao thì có thể tìm hiểu thêm về signed upload nhé).

Chúng ta cần kích hoạt unsigned upload trên bảng điều khiển (console). Chọn setting -> tab upload, cuộn tới nơi bạn có upload preset, chọn unsigned uploading.

Một preset mới được tạo ra với một chuỗi ngẫu nhiên. Sao chép nó lại vì bạn sẽ cần nó để điền vào project.

Cấu hình Upload video trên Cloudinary

Tiếp theo bạn lấy video từ bộ nhớ điện thoại, upload lên Cloudinary và Cloudinary sẽ giúp bạn convert video sang gif. Nhiệm vụ của bạn khá đơn giản, chỉ là download tập tin .gif về.

Sau khi cấu hình xong, chúng ta sẽ implement cho phần upload video trong MainActivity.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()
    
        }
    
    }

#Cấu hình thông số ảnh GIF

Nếu upload thành công, hàm onSuccess() sẽ được gọi. Hàm này cung cấp cho chúng ta requestId và những thông tin chi tiết từ việc upload.

Bạn có thể truy cập vào URL của những video vừa được upload bằng việc gọi hàm resultData[“url”]. Thay vì một video, bạn cần một tập tin.gif.

Chúng ta có thể dễ dàng có được nó bởi việc thay đổi phần mở rộng của một video thành .gif. Đại khái sẽ như thế này:

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")

Mỗi video được upload đều có một ID riêng biệt từ Cloudinary. ID này được truy cập nằng việc gọi hàm resultData["public_url"].

Giá trị đưa ra gifUrl là kết quả của việc lấy URL Cloudinary dựa trên tên cloud của bạn (thường giống như là res.cloudinary.com/{cloud name}/), được nối thêm dữ liệu transformations, resource type được truy cập.

unique id được lưu trữ trên đám mây, và để định dạng đầu ra (trong trường hợp demo của chúng ta thì là gif).

Mình sẽ giải thích kỹ hơn về các thông số trong đoạn code trên: Chiều cao, chiền rộng ảnh gif lần lượt là 232 và 174. Tỉ lệ khung là 12fps, ảnh GIF hai lần.

Nếu không có thiết lập trên, chúng ta có thể sẽ nhận được ảnh gif có dung lượng còn lớn cả video mà ta đã upload lên trước đó. Bạn có thể không rành những thông số này thì nên đọc thêm ở đây

#Tạo tính năng tải ảnh động GIF

Sau khi đã chuyển đổi video sang gif, chúng ta cần cung cấp thêm tính năng tải ảnh gif đó về máy điện thoại của người dùng.

Trong tập tin build.gradle, chúng ta sẽ thêm các thư viện dependency download:

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

Trên AppController.kt, chúng ta khởi tạo thư viện PRDownloader:

        override fun onCreate() {
            super.onCreate()
            // Initialize Cloudinary
            public class AppController : Application() {
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
        }
    }

Hàm này xử lí việc download các ảnh động GIF. Hàm RDownload.download()có ba tham số cần truyền vào:

  • Đường link file cần download.
  • Thư mục chứa file sẽ download.
  • Tên của file sẽ download.

Hàm này sẽ được gọi trong hàn onSuccess của UploadCallback sau khi gifUrl được tạo ra:

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

Ảnh gif được tải về sẽ lưu ở đây: Android/data/{app package name}/files.

Kết luận

Cloudinary cung cấp nhiều APIs giúp giảm gánh nặng lưu trữ và thao tác ảnh/video. Với tutorial này của mình chỉ là một phần nhỏ trong số các tính năng hay ho của Cloudinary.

Bạn có thể download trọn bộ source code của bài viết tại đây:

Như vậy, mình đã kết thúc tutorial về chuyển đổi video sang gif cho Android . Nếu có chỗ nào không hiểu thì comment phía bên dưới nhé.

Đọc thêm:

Dịch vụ phát triển ứng dụng mobile giá rẻ - chất lượng
Bài trướcDocker là gì? Tìm hiểu Docker từ chưa biết gì tới biết dùng
Bài tiếp theoTạo Tooltips trong Android không cần code
Tên đầy đủ là Dương Anh Sơn. Tốt nghiệp ĐH Bách Khoa Hà Nội. Mình bắt đầu nghiệp coder khi mà ra trường chẳng xin được việc đúng chuyên ngành. Mình tin rằng chỉ có chia sẻ kiến thức mới là cách học tập nhanh nhất. Các bạn góp ý bài viết của mình bằng cách comment bên dưới nhé !

2
Bình luận. Cùng nhau thảo luận nhé!

avatar
  Theo dõi bình luận  
Mới nhất Cũ nhất Nhiều voted nhất
Thông báo
Tùng Lâm
Guest
Tùng Lâm

Cho mình hỏi cái này có mất phí không ạ?