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

0
93

convert-video-to-gif

Giới thiệu tổng quan

1. GIFs là gì?

GIF (Graphics Interchange Format ) là một định dạng hình ảnh bitmap có khả năng chuyển động. Chúng có thể được dùng để truyền tải cảm xúc khi chúng ta không thể tự thể hiện được. Rất nhiều GIFs đã biến nhiều 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.

2. Dịch vụ 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 -giúp hỗ trợ upload, 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 sẽ Upload một video và chuyển đổi thành một GIF một cách dễ dàng.

Yêu cầu

  1. Bạn cần có một tài khoản Cloudinary. Nếu chưa bạn có thể đăng ký miễn phí tại đây.
  2. Android Studio 3.0: Android Studio là một IDE chính thức cho phát triển các ứng dụng Android. Bạn có thể tải phiên bản mới nhất tại đây

Tạo Android Project (Client side)

1. Create project

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)

Building a Video to GIF Android App Using Kotlin

Chúng ta sẽ hỗ trợ min SDK là level 16(Android 4.1). Min SDK là phiên bản Android cũ nhất mà ứng dụng có thể chạy được.

Building a Video to GIF Android App Using Kotlin

Sau đó bạn chọn Empty Activity, nhập tên Activity chính và finish. Như vậy là xong phần thiết lập cơ bản, chúng ta chuyển sang phần code nhé.

Building a Video to GIF Android App Using Kotlin

2. Installing Cloudinary

Để truy cập được các tính năng của Cloudinary, chúng ta cần chèn thêm dependency. Chúng ta 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 cấu hình (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

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. Xây dựng Layout

Đây là một snippet trong layout của chúng ta.

<?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, nó sẽ quay khi chúng ta upload hoặc download. Ngoài ra, chúng ta có một Button để chọn video từ bộ nhớ (local storage)

Impelement Upload video lên Cloudinary

Cloudinary hỗ trợ hai kiểu upload: signed and unsigned

  • 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.
  • 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ó sớm thôi.

Building a Video to GIF Android App Using Kotlin

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

Những phần việc của chúng ta sẽ được implement 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. Nó 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

Download tập tin GIF

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 tệp GIF. RDownload.download()có ba tham số: đường link file cần download, thư mục chứa file sẽ download và 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 hocủa Cloudinary. Bạn có thể tìm kiếm nguồn học code đầy đủ tại đây

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

Bình luận. Đặt câu hỏi cũng là một cách học

avatar
  Theo dõi bình luận  
Thông báo