Skip to content

Commit f050510

Browse files
committed
feat: add create video thumbnail and clear cache for thumbnail
1 parent 2952485 commit f050510

18 files changed

+376
-42
lines changed

README.md

+20
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,16 @@ const downloadFileUrl = await download(url, (progress) => {
299299
});
300300
```
301301

302+
### Video Thumbnail
303+
304+
```js
305+
import { createVideoThumbnail, clearCache } from 'react-native-compressor';
306+
307+
const thumbnail = await createVideoThumbnail(videoUri);
308+
309+
await clearCache(); // this will clear cache of thumbnails cache directory
310+
```
311+
302312
# API
303313

304314
## Image
@@ -421,6 +431,16 @@ type FileSystemUploadOptions = (
421431

422432
- ##### download: ( fileUrl: string, downloadProgress?: (progress: number) => void, progressDivider?: number ) => Promise< string >
423433

434+
### Create Video Thumbnail and clear cache
435+
436+
- #### createVideoThumbnail( fileUrl: string, options: {header:Object} ): Promise<{ path: string;size: number; mime: string; width: number; height: number; }>
437+
438+
it will save the thumbnail of the video into the cache directory and return the thumbnail URI which you can display
439+
440+
- #### clearCache(cacheDir?: string): Promise< string >
441+
442+
it will clear the cache that was created from createVideoThumbnail, in future this clear cache will be totally customized
443+
424444
### Get Metadata Of Video
425445

426446
if you want to get metadata of video than you can use this function

android/build.gradle

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
buildscript {
22
// Buildscript is evaluated before everything else so we can't use getExtOrDefault
33
def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["Compressor_kotlinVersion"]
4-
4+
55
repositories {
66
google()
77
mavenCentral()
@@ -110,6 +110,7 @@ dependencies {
110110
//noinspection GradleDynamicVersion
111111
implementation "com.facebook.react:react-native:+"
112112
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
113+
implementation 'commons-io:commons-io:2.8.0'
113114
implementation 'io.github.lizhangqu:coreprogress:1.0.2'
114115
implementation 'com.github.numandev1:VideoCompressor:1a262bba37'
115116
}

android/src/main/java/com/reactnativecompressor/CompressorModule.kt

+12
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.facebook.react.bridge.ReactMethod
88
import com.facebook.react.bridge.ReadableMap
99
import com.reactnativecompressor.Audio.AudioMain
1010
import com.reactnativecompressor.Image.ImageMain
11+
import com.reactnativecompressor.Utils.CreateVideoThumbnail
1112
import com.reactnativecompressor.Utils.Downloader
1213
import com.reactnativecompressor.Utils.EventEmitterHandler
1314
import com.reactnativecompressor.Utils.Uploader
@@ -20,6 +21,7 @@ class CompressorModule(private val reactContext: ReactApplicationContext) : Comp
2021
private val imageMain: ImageMain = ImageMain(reactContext)
2122
private val videoMain: VideoMain = VideoMain(reactContext)
2223
private val audioMain: AudioMain = AudioMain(reactContext)
24+
private val videoThumbnail: CreateVideoThumbnail = CreateVideoThumbnail(reactContext)
2325

2426
override fun initialize() {
2527
super.initialize()
@@ -147,6 +149,16 @@ class CompressorModule(private val reactContext: ReactApplicationContext) : Comp
147149
}
148150
}
149151

152+
@ReactMethod
153+
override fun createVideoThumbnail(fileUrl:String, options:ReadableMap, promise:Promise) {
154+
videoThumbnail.create(fileUrl,options,promise)
155+
}
156+
157+
@ReactMethod
158+
override fun clearCache(cacheDir:String?, promise:Promise) {
159+
CreateVideoThumbnail.clearCache(cacheDir, promise, reactContext)
160+
}
161+
150162
@ReactMethod
151163
override fun addListener(eventName: String) {
152164
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package com.reactnativecompressor.Utils
2+
3+
import android.content.Context
4+
import android.graphics.Bitmap
5+
import android.graphics.BitmapFactory
6+
import android.media.MediaMetadataRetriever
7+
import android.net.Uri
8+
import android.os.Build
9+
import android.text.TextUtils
10+
import android.webkit.URLUtil
11+
import androidx.annotation.RequiresApi
12+
import com.facebook.react.bridge.Arguments
13+
import com.facebook.react.bridge.GuardedResultAsyncTask
14+
import com.facebook.react.bridge.Promise
15+
import com.facebook.react.bridge.ReactApplicationContext
16+
import com.facebook.react.bridge.ReactContext
17+
import com.facebook.react.bridge.ReactMethod
18+
import com.facebook.react.bridge.ReadableMap
19+
import java.io.File
20+
import java.io.FileOutputStream
21+
import java.io.IOException
22+
import java.io.OutputStream
23+
import java.io.UnsupportedEncodingException
24+
import java.lang.ref.WeakReference
25+
import java.net.URLDecoder
26+
import java.util.UUID
27+
28+
29+
class CreateVideoThumbnail(private val reactContext: ReactApplicationContext) {
30+
@ReactMethod
31+
fun create(fileUrl:String,options: ReadableMap, promise: Promise) {
32+
ProcessDataTask(reactContext,fileUrl, promise, options).execute()
33+
}
34+
35+
private class ProcessDataTask(reactContext: ReactContext,private val filePath:String, private val promise: Promise, private val options: ReadableMap) : GuardedResultAsyncTask<ReadableMap?>(reactContext.exceptionHandler) {
36+
private val weakContext: WeakReference<Context>
37+
38+
init {
39+
weakContext = WeakReference(reactContext.applicationContext)
40+
}
41+
42+
@RequiresApi(Build.VERSION_CODES.N)
43+
override fun doInBackgroundGuarded(): ReadableMap? {
44+
val format = "jpeg"
45+
val cacheName = if (options.hasKey("cacheName")) options.getString("cacheName") else ""
46+
val thumbnailDir = weakContext.get()!!.applicationContext.cacheDir.absolutePath + "/thumbnails"
47+
val cacheDir = createDirIfNotExists(thumbnailDir)
48+
if (!TextUtils.isEmpty(cacheName)) {
49+
val file = File(thumbnailDir, "$cacheName.$format")
50+
if (file.exists()) {
51+
val map = Arguments.createMap()
52+
map.putString("path", "file://" + file.absolutePath)
53+
val image = BitmapFactory.decodeFile(file.absolutePath)
54+
map.putDouble("size", image.byteCount.toDouble())
55+
map.putString("mime", "image/$format")
56+
map.putDouble("width", image.width.toDouble())
57+
map.putDouble("height", image.height.toDouble())
58+
return map
59+
}
60+
}
61+
val headers: Map<String, String> = if (options.hasKey("headers")) options.getMap("headers")!!.toHashMap() as Map<String, String> else HashMap<String, String>()
62+
val fileName = if (TextUtils.isEmpty(cacheName)) "thumb-" + UUID.randomUUID().toString() else "$cacheName.$format"
63+
var fOut: OutputStream? = null
64+
try {
65+
val file = File(cacheDir, fileName)
66+
val context = weakContext.get()
67+
val image = getBitmapAtTime(context, filePath, 0, headers)
68+
file.createNewFile()
69+
fOut = FileOutputStream(file)
70+
71+
// 100 means no compression, the lower you go, the stronger the compression
72+
image.compress(Bitmap.CompressFormat.JPEG, 90, fOut)
73+
fOut.flush()
74+
fOut.close()
75+
76+
val map = Arguments.createMap()
77+
map.putString("path", "file://" + file.absolutePath)
78+
map.putDouble("size", image.byteCount.toDouble())
79+
map.putString("mime", "image/$format")
80+
map.putDouble("width", image.width.toDouble())
81+
map.putDouble("height", image.height.toDouble())
82+
return map
83+
} catch (e: Exception) {
84+
promise.reject("CreateVideoThumbnail_ERROR", e)
85+
}
86+
return null
87+
}
88+
89+
override fun onPostExecuteGuarded(readableArray: ReadableMap?) {
90+
promise.resolve(readableArray)
91+
}
92+
}
93+
94+
companion object {
95+
// delete previously added files one by one untill requred space is available
96+
fun clearCache(cacheDir: String?,promise:Promise, reactContext: ReactApplicationContext) {
97+
val cacheDirectory=cacheDir?.takeIf { it.isNotEmpty() } ?:"/thumbnails"
98+
val thumbnailDir: String = reactContext.getApplicationContext().getCacheDir().getAbsolutePath() + cacheDirectory
99+
val thumbnailDirFile = createDirIfNotExists(thumbnailDir)
100+
101+
if (thumbnailDirFile != null) {
102+
val files = thumbnailDirFile.listFiles()
103+
104+
// Loop through the files and delete them
105+
if (files != null) {
106+
for (file in files) {
107+
if (file.isFile) {
108+
file.delete()
109+
}
110+
}
111+
}
112+
}
113+
promise.resolve("done")
114+
}
115+
116+
private fun createDirIfNotExists(path: String): File {
117+
val dir = File(path)
118+
if (dir.exists()) {
119+
return dir
120+
}
121+
try {
122+
dir.mkdirs()
123+
// Add .nomedia to hide the thumbnail directory from gallery
124+
val noMedia = File(path, ".nomedia")
125+
noMedia.createNewFile()
126+
} catch (e: IOException) {
127+
e.printStackTrace()
128+
}
129+
return dir
130+
}
131+
132+
private fun getBitmapAtTime(context: Context?, filePath: String?, time: Int, headers: Map<String, String>): Bitmap {
133+
val retriever = MediaMetadataRetriever()
134+
if (URLUtil.isFileUrl(filePath)) {
135+
val decodedPath: String?
136+
decodedPath = try {
137+
URLDecoder.decode(filePath, "UTF-8")
138+
} catch (e: UnsupportedEncodingException) {
139+
filePath
140+
}
141+
retriever.setDataSource(decodedPath!!.replace("file://", ""))
142+
} else if (filePath!!.contains("content://")) {
143+
retriever.setDataSource(context, Uri.parse(filePath))
144+
} else {
145+
check(Build.VERSION.SDK_INT >= 14) { "Remote videos aren't supported on sdk_version < 14" }
146+
retriever.setDataSource(filePath, headers)
147+
}
148+
val image = retriever.getFrameAtTime((time * 1000).toLong(), MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
149+
try {
150+
retriever.release()
151+
} catch (e: IOException) {
152+
throw RuntimeException(e)
153+
}
154+
checkNotNull(image) { "File doesn't exist or not supported" }
155+
return image
156+
}
157+
}
158+
}

android/src/oldarch/CompressorSpec.kt

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ abstract class CompressorSpec(context: ReactApplicationContext?) : ReactContextB
2727
abstract fun download(fileUrl: String, options: ReadableMap, promise: Promise)
2828
abstract fun activateBackgroundTask(options: ReadableMap, promise: Promise)
2929
abstract fun deactivateBackgroundTask(options: ReadableMap, promise: Promise)
30+
abstract fun createVideoThumbnail(fileUrl: String?, options: ReadableMap?, promise: Promise)
31+
abstract fun clearCache(cacheDir: String?, promise: com.facebook.react.bridge.Promise?)
3032
abstract fun addListener(eventName: String)
3133
abstract fun removeListeners(count: Double)
3234
}

example/ios/Podfile.lock

+2-8
Original file line numberDiff line numberDiff line change
@@ -881,7 +881,7 @@ PODS:
881881
- React-Codegen
882882
- React-RCTFabric
883883
- ReactCommon/turbomodule/core
884-
- react-native-compressor (1.7.2):
884+
- react-native-compressor (1.8.0):
885885
- hermes-engine
886886
- NextLevelSessionExporter
887887
- RCT-Folly (= 2021.07.22.00)
@@ -898,8 +898,6 @@ PODS:
898898
- ReactCommon/turbomodule/bridging
899899
- ReactCommon/turbomodule/core
900900
- Yoga
901-
- react-native-create-thumbnail (1.6.4):
902-
- React-Core
903901
- react-native-document-picker (9.0.1):
904902
- RCT-Folly
905903
- RCTRequired
@@ -1210,7 +1208,6 @@ DEPENDENCIES:
12101208
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
12111209
- "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)"
12121210
- react-native-compressor (from `../..`)
1213-
- react-native-create-thumbnail (from `../node_modules/react-native-create-thumbnail`)
12141211
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
12151212
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
12161213
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
@@ -1299,8 +1296,6 @@ EXTERNAL SOURCES:
12991296
:path: "../node_modules/@react-native-camera-roll/camera-roll"
13001297
react-native-compressor:
13011298
:path: "../.."
1302-
react-native-create-thumbnail:
1303-
:path: "../node_modules/react-native-create-thumbnail"
13041299
react-native-document-picker:
13051300
:path: "../node_modules/react-native-document-picker"
13061301
react-native-get-random-values:
@@ -1386,8 +1381,7 @@ SPEC CHECKSUMS:
13861381
React-jsinspector: aaed4cf551c4a1c98092436518c2d267b13a673f
13871382
React-logger: da1ebe05ae06eb6db4b162202faeafac4b435e77
13881383
react-native-cameraroll: 5d9523136a929b58f092fd7f0a9a13367a4b46e3
1389-
react-native-compressor: 3d3244f0256be3866da06f7579b47cb0d46f08ac
1390-
react-native-create-thumbnail: e022bcdcba8a0b4529a50d3fa1a832ec921be39d
1384+
react-native-compressor: 7afa42104baf644c6b3eaad024508306b71379a9
13911385
react-native-document-picker: c9ac93d7b511413f4a0ed61c92ff6c7b1bcf4f94
13921386
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
13931387
react-native-image-picker: 9b4b1d0096500050cbdabf8f4fd00b771065d983

example/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
"pretty-bytes": "^6.1.1",
2525
"react": "18.2.0",
2626
"react-native": "0.72.4",
27-
"react-native-create-thumbnail": "^1.6.4",
2827
"react-native-document-picker": "^9.0.1",
2928
"react-native-fs": "^2.20.0",
3029
"react-native-get-random-values": "^1.9.0",

example/patches/react-native-create-thumbnail+1.6.4.patch

-18
This file was deleted.

example/src/Screens/Video/index.tsx

+19-9
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import React, { useState, useEffect, useRef } from 'react';
22
import { View, Text, Button, Image, Alert, Platform } from 'react-native';
3-
import { Video, getRealPath, backgroundUpload } from 'react-native-compressor';
3+
import {
4+
Video,
5+
getRealPath,
6+
backgroundUpload,
7+
createVideoThumbnail,
8+
clearCache,
9+
} from 'react-native-compressor';
410
import * as ImagePicker from 'react-native-image-picker';
5-
import { createThumbnail } from 'react-native-create-thumbnail';
611
import CameraRoll from '@react-native-camera-roll/camera-roll';
712
import prettyBytes from 'pretty-bytes';
813
import { getFileInfo } from '../../Utils';
@@ -25,9 +30,7 @@ export default function App() {
2530

2631
useEffect(() => {
2732
if (!sourceVideo) return;
28-
createThumbnail({
29-
url: sourceVideo,
30-
})
33+
createVideoThumbnail(sourceVideo, {})
3134
.then((response) => setSourceVideoThumbnail(response.path))
3235
.catch((error) => console.log({ error }));
3336
(async () => {
@@ -39,9 +42,7 @@ export default function App() {
3942
useEffect(() => {
4043
if (!compressedVideo) return;
4144
setcompressedVideoThumbnail(sourceVideoThumbnail);
42-
createThumbnail({
43-
url: compressedVideo,
44-
})
45+
createVideoThumbnail(compressedVideo)
4546
.then((response) => setcompressedVideoThumbnail(response.path))
4647
.catch((error) => {
4748
console.log({ errorThumnail: error });
@@ -218,12 +219,20 @@ export default function App() {
218219
});
219220
const phUrl = photos.page_info.end_cursor;
220221
setSourceVideo(phUrl);
221-
console.log('nomi', phUrl);
222222
if (phUrl?.includes('ph://')) {
223223
const realPath = await getRealPath(phUrl, 'video');
224224
console.log('old path==>', phUrl, 'realPath ==>', realPath);
225225
}
226226
};
227+
228+
const clearThumbnailCache = () => {
229+
clearCache()
230+
.then(() => {
231+
console.log('done');
232+
})
233+
.catch((error: any) => console.log(error));
234+
};
235+
227236
return (
228237
<View style={{ flex: 1 }}>
229238
<ProgressBar ref={progressRef} />
@@ -285,6 +294,7 @@ export default function App() {
285294
onPress={onPressRemoteVideo}
286295
/>
287296
<Button title="Cancel Compression" onPress={cancelCompression} />
297+
<Button title="clear thumbnail cache" onPress={clearThumbnailCache} />
288298
<Text>Put app in background and check console output</Text>
289299
<View
290300
style={{

0 commit comments

Comments
 (0)