expo-video-metadata 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +5 -0
- package/README.md +41 -0
- package/android/build.gradle +89 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/videometadata/Exceptions.kt +9 -0
- package/android/src/main/java/expo/modules/videometadata/ExpoVideoMetadataModule.kt +203 -0
- package/android/src/main/java/expo/modules/videometadata/VideoMetadataOptions.kt +9 -0
- package/build/ExpoVideoMetadata.types.d.ts +57 -0
- package/build/ExpoVideoMetadata.types.d.ts.map +1 -0
- package/build/ExpoVideoMetadata.types.js +2 -0
- package/build/ExpoVideoMetadata.types.js.map +1 -0
- package/build/ExpoVideoMetadataModule.d.ts +3 -0
- package/build/ExpoVideoMetadataModule.d.ts.map +1 -0
- package/build/ExpoVideoMetadataModule.js +3 -0
- package/build/ExpoVideoMetadataModule.js.map +1 -0
- package/build/ExpoVideoMetadataModule.web.d.ts +7 -0
- package/build/ExpoVideoMetadataModule.web.d.ts.map +1 -0
- package/build/ExpoVideoMetadataModule.web.js +9 -0
- package/build/ExpoVideoMetadataModule.web.js.map +1 -0
- package/build/index.d.ts +12 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +15 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/ExpoVideoMetadata.podspec +27 -0
- package/ios/ExpoVideoMetadataExceptions.swift +7 -0
- package/ios/ExpoVideoMetadataModule.swift +110 -0
- package/ios/ExpoVideoMetadataOptions.swift +5 -0
- package/package.json +42 -0
- package/src/ExpoVideoMetadata.types.ts +61 -0
- package/src/ExpoVideoMetadataModule.ts +2 -0
- package/src/ExpoVideoMetadataModule.web.ts +16 -0
- package/src/index.ts +22 -0
- package/tsconfig.json +9 -0
package/.eslintrc.js
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# expo-video-metadata
|
|
2
|
+
|
|
3
|
+
Provides a function that let you get some metadata from video files, like the duration, width, height, fps, codec, hasAudio, orientation, audioChannels, audioCodec, audioSampleRate etc. Check the exported types for more information. Web support is not available yet but it is planned.
|
|
4
|
+
|
|
5
|
+
# Installation in bare React Native projects
|
|
6
|
+
|
|
7
|
+
This package needs **Expo SDK 50** or **higher**, as it uses FileSystem APIs that were added in that version. This package adds native code to your project and does not work with Expo Go. Please use a custom dev client or build a standalone app.
|
|
8
|
+
|
|
9
|
+
For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing (SDK 50+). This just adds ~150KB to your final app size and is the easiest way to get started and it works with and without Expo projects.
|
|
10
|
+
|
|
11
|
+
### Add the package to your npm dependencies
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
npx expo install expo-video-metadata
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Configure for iOS
|
|
18
|
+
|
|
19
|
+
Run `npx pod-install` after installing the npm package.
|
|
20
|
+
|
|
21
|
+
### Configure for Android
|
|
22
|
+
|
|
23
|
+
No additional set up necessary.
|
|
24
|
+
|
|
25
|
+
# API
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { getVideoInfoAsync } from 'expo-video-metadata';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Gets metadata from a video file.
|
|
32
|
+
* @param sourceFilename An URI of the video, local or remote.
|
|
33
|
+
* @return Returns a promise that resolves to a `VideoInfoResult` object.
|
|
34
|
+
*/
|
|
35
|
+
const videoInfo = await getVideoInfoAsync(sourceFileName: string): Promise<VideoInfo>;
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
# Info
|
|
40
|
+
|
|
41
|
+
I am planning to add more metadata to the result object. If you need something specific, please open an issue or a PR.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
apply plugin: 'kotlin-android'
|
|
3
|
+
apply plugin: 'maven-publish'
|
|
4
|
+
|
|
5
|
+
group = 'expo.modules.videometadata'
|
|
6
|
+
version = '0.1.0'
|
|
7
|
+
|
|
8
|
+
buildscript {
|
|
9
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
10
|
+
if (expoModulesCorePlugin.exists()) {
|
|
11
|
+
apply from: expoModulesCorePlugin
|
|
12
|
+
applyKotlinExpoModulesCorePlugin()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Simple helper that allows the root project to override versions declared by this library.
|
|
16
|
+
ext.safeExtGet = { prop, fallback ->
|
|
17
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Ensures backward compatibility
|
|
21
|
+
ext.getKotlinVersion = {
|
|
22
|
+
if (ext.has("kotlinVersion")) {
|
|
23
|
+
ext.kotlinVersion()
|
|
24
|
+
} else {
|
|
25
|
+
ext.safeExtGet("kotlinVersion", "1.8.10")
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
repositories {
|
|
30
|
+
mavenCentral()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
dependencies {
|
|
34
|
+
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
afterEvaluate {
|
|
39
|
+
publishing {
|
|
40
|
+
publications {
|
|
41
|
+
release(MavenPublication) {
|
|
42
|
+
from components.release
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
repositories {
|
|
46
|
+
maven {
|
|
47
|
+
url = mavenLocal().url
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
android {
|
|
54
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 33)
|
|
55
|
+
|
|
56
|
+
compileOptions {
|
|
57
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
58
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
kotlinOptions {
|
|
62
|
+
jvmTarget = JavaVersion.VERSION_17.majorVersion
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
namespace "expo.modules.videometadata"
|
|
66
|
+
defaultConfig {
|
|
67
|
+
minSdkVersion safeExtGet("minSdkVersion", 21)
|
|
68
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 33)
|
|
69
|
+
versionCode 1
|
|
70
|
+
versionName "0.1.0"
|
|
71
|
+
}
|
|
72
|
+
lintOptions {
|
|
73
|
+
abortOnError false
|
|
74
|
+
}
|
|
75
|
+
publishing {
|
|
76
|
+
singleVariant("release") {
|
|
77
|
+
withSourcesJar()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
repositories {
|
|
83
|
+
mavenCentral()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
dependencies {
|
|
87
|
+
implementation project(':expo-modules-core')
|
|
88
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
|
|
89
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
package expo.modules.videometadata
|
|
2
|
+
|
|
3
|
+
import android.media.MediaMetadataRetriever
|
|
4
|
+
import android.net.Uri
|
|
5
|
+
import android.util.Log
|
|
6
|
+
import android.webkit.URLUtil
|
|
7
|
+
import expo.modules.core.errors.ModuleDestroyedException
|
|
8
|
+
import expo.modules.interfaces.filesystem.Permission
|
|
9
|
+
import expo.modules.kotlin.Promise
|
|
10
|
+
import expo.modules.kotlin.exception.CodedException
|
|
11
|
+
import expo.modules.kotlin.exception.Exceptions
|
|
12
|
+
import expo.modules.kotlin.modules.Module
|
|
13
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
14
|
+
import kotlinx.coroutines.CoroutineScope
|
|
15
|
+
import kotlinx.coroutines.Dispatchers
|
|
16
|
+
import kotlinx.coroutines.cancel
|
|
17
|
+
import kotlinx.coroutines.launch
|
|
18
|
+
import java.io.File
|
|
19
|
+
import android.media.MediaExtractor
|
|
20
|
+
import android.media.MediaFormat
|
|
21
|
+
import java.math.BigDecimal
|
|
22
|
+
import java.math.RoundingMode
|
|
23
|
+
|
|
24
|
+
class ExpoVideoMetadataModule : Module() {
|
|
25
|
+
private val context
|
|
26
|
+
get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
|
|
27
|
+
private val moduleCoroutineScope = CoroutineScope(Dispatchers.IO)
|
|
28
|
+
|
|
29
|
+
override fun definition() = ModuleDefinition {
|
|
30
|
+
Name("ExpoVideoMetadata")
|
|
31
|
+
|
|
32
|
+
AsyncFunction("getVideoInfo") { sourceFilename: String, options: ExpoVideoMetadataOptions, promise: Promise ->
|
|
33
|
+
if (URLUtil.isFileUrl(sourceFilename) && !isAllowedToRead(Uri.decode(sourceFilename).replace("file://", ""))) {
|
|
34
|
+
throw VideoFileException()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
withModuleScope(promise) {
|
|
38
|
+
try {
|
|
39
|
+
val retriever = MediaMetadataRetriever()
|
|
40
|
+
val extractor = MediaExtractor()
|
|
41
|
+
|
|
42
|
+
var fileSize: Long? = null
|
|
43
|
+
|
|
44
|
+
if (URLUtil.isFileUrl(sourceFilename)) {
|
|
45
|
+
retriever.setDataSource(Uri.decode(sourceFilename).replace("file://", ""))
|
|
46
|
+
extractor.setDataSource(Uri.decode(sourceFilename).replace("file://", ""))
|
|
47
|
+
fileSize = File(sourceFilename.replace("file://", "")).length()
|
|
48
|
+
} else if (URLUtil.isContentUrl(sourceFilename)) {
|
|
49
|
+
val fileUri = Uri.parse(sourceFilename)
|
|
50
|
+
fileSize = File(sourceFilename).length()
|
|
51
|
+
context.contentResolver.openFileDescriptor(fileUri, "r")?.use { parcelFileDescriptor ->
|
|
52
|
+
retriever.setDataSource(parcelFileDescriptor.fileDescriptor)
|
|
53
|
+
extractor.setDataSource(parcelFileDescriptor.fileDescriptor)
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
retriever.setDataSource(sourceFilename, options.headers)
|
|
57
|
+
extractor.setDataSource(sourceFilename, options.headers)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// extract metadata
|
|
61
|
+
val durationMs = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() ?: 0L
|
|
62
|
+
val duration = BigDecimal(durationMs)
|
|
63
|
+
.divide(BigDecimal(1000), 15, RoundingMode.HALF_UP)
|
|
64
|
+
.toDouble()
|
|
65
|
+
|
|
66
|
+
val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toIntOrNull()
|
|
67
|
+
val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toIntOrNull()
|
|
68
|
+
val bitrate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)?.toIntOrNull()
|
|
69
|
+
val rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toIntOrNull()
|
|
70
|
+
val hasAudio = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO) != null
|
|
71
|
+
|
|
72
|
+
// release
|
|
73
|
+
retriever.release()
|
|
74
|
+
|
|
75
|
+
// Additional metadata can be extracted here
|
|
76
|
+
var audioChannels: Int? = null
|
|
77
|
+
var audioSampleRate: Int? = null
|
|
78
|
+
var audioCodec: String? = null
|
|
79
|
+
var videoCodec: String? = null
|
|
80
|
+
var frameRate: Float? = null
|
|
81
|
+
|
|
82
|
+
val numTracks = extractor.trackCount
|
|
83
|
+
for (i in 0 until numTracks) {
|
|
84
|
+
val format = extractor.getTrackFormat(i)
|
|
85
|
+
val mimeType = format.getString(MediaFormat.KEY_MIME) ?: continue
|
|
86
|
+
if (mimeType.startsWith("audio/")) {
|
|
87
|
+
audioChannels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
|
|
88
|
+
audioSampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
|
|
89
|
+
audioCodec = mapMimeTypeToCodecName(mimeType)
|
|
90
|
+
} else if (mimeType.startsWith("video/")) {
|
|
91
|
+
videoCodec = mapMimeTypeToCodecName(mimeType)
|
|
92
|
+
|
|
93
|
+
// extract video frameRate
|
|
94
|
+
if (format.containsKey(MediaFormat.KEY_FRAME_RATE)) {
|
|
95
|
+
frameRate = try {
|
|
96
|
+
// Try to get frame rate as Integer and convert to Float
|
|
97
|
+
format.getInteger(MediaFormat.KEY_FRAME_RATE).toFloat()
|
|
98
|
+
} catch (e: Exception) {
|
|
99
|
+
// If Integer retrieval fails, try as Float
|
|
100
|
+
format.getFloat(MediaFormat.KEY_FRAME_RATE)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
extractor.release()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
promise.resolve(
|
|
110
|
+
mapOf(
|
|
111
|
+
"audioChannels" to audioChannels,
|
|
112
|
+
"duration" to duration,
|
|
113
|
+
"width" to width,
|
|
114
|
+
"height" to height,
|
|
115
|
+
"bitrate" to bitrate,
|
|
116
|
+
"fileSize" to fileSize,
|
|
117
|
+
"hasAudio" to hasAudio,
|
|
118
|
+
"audioCodec" to audioCodec,
|
|
119
|
+
"orientation" to getOrientation(rotation),
|
|
120
|
+
"audioSampleRate" to audioSampleRate,
|
|
121
|
+
"audioCodec" to audioCodec,
|
|
122
|
+
"codec" to videoCodec,
|
|
123
|
+
"fps" to frameRate
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
} catch (e: Exception) {
|
|
127
|
+
Log.e(TAG, "Error retrieving video metadata: ${e.message}", e)
|
|
128
|
+
promise.reject(ERROR_TAG, "Failed to retrieve video metadata", e)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
OnDestroy {
|
|
135
|
+
try {
|
|
136
|
+
moduleCoroutineScope.cancel(ModuleDestroyedException())
|
|
137
|
+
} catch (e: IllegalStateException) {
|
|
138
|
+
Log.e(TAG, "The scope does not have a job in it")
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private fun isAllowedToRead(url: String): Boolean {
|
|
144
|
+
val permissionModuleInterface = appContext.filePermission
|
|
145
|
+
?: throw FilePermissionsModuleNotFound()
|
|
146
|
+
return permissionModuleInterface.getPathPermissions(context, url).contains(Permission.READ)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private fun getOrientation(rotation: Int?): String {
|
|
150
|
+
return when (rotation) {
|
|
151
|
+
0 -> "LandscapeRight"
|
|
152
|
+
90 -> "Portrait"
|
|
153
|
+
180 -> "LandscapeLeft"
|
|
154
|
+
270 -> "PortraitUpsideDown"
|
|
155
|
+
else -> "LandscapeRight" // Default or unknown rotation
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
private fun mapMimeTypeToCodecName(mimeType: String): String {
|
|
161
|
+
return when {
|
|
162
|
+
mimeType.startsWith("audio/") -> {
|
|
163
|
+
when {
|
|
164
|
+
mimeType.contains("mp4a-latm") -> "aac" // AAC Audio
|
|
165
|
+
mimeType.contains("ac3") -> "ac3" // AC3 Audio
|
|
166
|
+
mimeType.contains("opus") -> "opus" // Opus Audio
|
|
167
|
+
mimeType.contains("vorbis") -> "vorbis" // Vorbis Audio
|
|
168
|
+
mimeType.contains("flac") -> "flac" // FLAC Audio
|
|
169
|
+
// Add more audio mappings as needed
|
|
170
|
+
else -> mimeType.substringAfter("audio/")
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
mimeType.startsWith("video/") -> {
|
|
174
|
+
when {
|
|
175
|
+
mimeType.contains("avc") || mimeType.contains("h264") -> "avc1" // H.264/AVC Video
|
|
176
|
+
mimeType.contains("hev") || mimeType.contains("h265") -> "hev1" // H.265/HEVC Video
|
|
177
|
+
mimeType.contains("vp9") -> "vp9" // VP9 Video
|
|
178
|
+
mimeType.contains("vp8") -> "vp8" // VP8 Video
|
|
179
|
+
mimeType.contains("mp4v-es") -> "mp4v" // MPEG-4 Video
|
|
180
|
+
// Add more video mappings as needed
|
|
181
|
+
else -> mimeType.substringAfter("video/")
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else -> mimeType
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
private inline fun withModuleScope(promise: Promise, crossinline block: () -> Unit) = moduleCoroutineScope.launch {
|
|
190
|
+
try {
|
|
191
|
+
block()
|
|
192
|
+
} catch (e: CodedException) {
|
|
193
|
+
promise.reject(e)
|
|
194
|
+
} catch (e: ModuleDestroyedException) {
|
|
195
|
+
promise.reject(TAG, "ExpoVideoMetadata module destroyed", e)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
companion object {
|
|
200
|
+
private const val TAG = "ExpoVideoMetadata"
|
|
201
|
+
private const val ERROR_TAG = "E_VIDEO_METADATA"
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type VideoInfoResult = {
|
|
2
|
+
/**
|
|
3
|
+
* Duration of the video in seconds (float).
|
|
4
|
+
*/
|
|
5
|
+
duration: number;
|
|
6
|
+
/**
|
|
7
|
+
* Tells if the video has a audio track. If the video has no audio track, its considered a mute video.
|
|
8
|
+
*/
|
|
9
|
+
hasAudio: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Width of the video in pixels.
|
|
12
|
+
*/
|
|
13
|
+
width: number;
|
|
14
|
+
/**
|
|
15
|
+
* Height of the video in pixels.
|
|
16
|
+
*/
|
|
17
|
+
height: number;
|
|
18
|
+
/**
|
|
19
|
+
* Frame rate of the video in frames per second.
|
|
20
|
+
*/
|
|
21
|
+
fps: number;
|
|
22
|
+
/**
|
|
23
|
+
* Bit rate of the video in bits per second.
|
|
24
|
+
*/
|
|
25
|
+
bitRate: number;
|
|
26
|
+
/**
|
|
27
|
+
* File size of the video in bytes. Works only for local files, returns 0 for remote files.
|
|
28
|
+
*/
|
|
29
|
+
fileSize: number;
|
|
30
|
+
/**
|
|
31
|
+
* Video codec.
|
|
32
|
+
*/
|
|
33
|
+
codec: string;
|
|
34
|
+
/**
|
|
35
|
+
* Video orientation.
|
|
36
|
+
*/
|
|
37
|
+
orientation: "Portrait" | "PortraitUpsideDown" | "LandscapeRight" | "LandscapeLeft";
|
|
38
|
+
/**
|
|
39
|
+
* Audio sample rate of the video in samples per second.
|
|
40
|
+
*/
|
|
41
|
+
audioSampleRate: number;
|
|
42
|
+
/**
|
|
43
|
+
* Audio channel count of the video.
|
|
44
|
+
*/
|
|
45
|
+
audioChannels: number;
|
|
46
|
+
/**
|
|
47
|
+
* Audio codec of the video.
|
|
48
|
+
*/
|
|
49
|
+
audioCodec: string;
|
|
50
|
+
};
|
|
51
|
+
export type VideoInfoOptions = {
|
|
52
|
+
/**
|
|
53
|
+
* In case `sourceFilename` is a remote URI, `headers` object is passed in a network request.
|
|
54
|
+
*/
|
|
55
|
+
headers?: Record<string, string>;
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=ExpoVideoMetadata.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoVideoMetadata.types.d.ts","sourceRoot":"","sources":["../src/ExpoVideoMetadata.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG;IAC5B;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,WAAW,EACP,UAAU,GACV,oBAAoB,GACpB,gBAAgB,GAChB,eAAe,CAAC;IACpB;;OAEG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoVideoMetadata.types.js","sourceRoot":"","sources":["../src/ExpoVideoMetadata.types.ts"],"names":[],"mappings":"","sourcesContent":["export type VideoInfoResult = {\n /**\n * Duration of the video in seconds (float).\n */\n duration: number;\n /**\n * Tells if the video has a audio track. If the video has no audio track, its considered a mute video.\n */\n hasAudio: boolean;\n /**\n * Width of the video in pixels.\n */\n width: number;\n /**\n * Height of the video in pixels.\n */\n height: number;\n /**\n * Frame rate of the video in frames per second.\n */\n fps: number;\n /**\n * Bit rate of the video in bits per second.\n */\n bitRate: number;\n /**\n * File size of the video in bytes. Works only for local files, returns 0 for remote files.\n */\n fileSize: number;\n /**\n * Video codec.\n */\n codec: string;\n /**\n * Video orientation.\n */\n orientation:\n | \"Portrait\"\n | \"PortraitUpsideDown\"\n | \"LandscapeRight\"\n | \"LandscapeLeft\";\n /**\n * Audio sample rate of the video in samples per second.\n */\n audioSampleRate: number;\n /**\n * Audio channel count of the video.\n */\n audioChannels: number;\n /**\n * Audio codec of the video.\n */\n audioCodec: string;\n};\n\nexport type VideoInfoOptions = {\n /**\n * In case `sourceFilename` is a remote URI, `headers` object is passed in a network request.\n */\n headers?: Record<string, string>;\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoVideoMetadataModule.d.ts","sourceRoot":"","sources":["../src/ExpoVideoMetadataModule.ts"],"names":[],"mappings":";AACA,wBAAwD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoVideoMetadataModule.js","sourceRoot":"","sources":["../src/ExpoVideoMetadataModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,eAAe,mBAAmB,CAAC,mBAAmB,CAAC,CAAC","sourcesContent":["import { requireNativeModule } from \"expo-modules-core\";\nexport default requireNativeModule(\"ExpoVideoMetadata\");\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { VideoInfoOptions, VideoInfoResult } from "./ExpoVideoMetadata.types";
|
|
2
|
+
declare const _default: {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
getVideoInfoAsync(sourceFilename: string, options?: VideoInfoOptions): Promise<VideoInfoResult>;
|
|
5
|
+
};
|
|
6
|
+
export default _default;
|
|
7
|
+
//# sourceMappingURL=ExpoVideoMetadataModule.web.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoVideoMetadataModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoVideoMetadataModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EAChB,MAAM,2BAA2B,CAAC;;;sCAOf,MAAM,YACb,gBAAgB,GACxB,QAAQ,eAAe,CAAC;;AAP7B,wBAUE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoVideoMetadataModule.web.js","sourceRoot":"","sources":["../src/ExpoVideoMetadataModule.web.ts"],"names":[],"mappings":"AAKA,eAAe;IACb,IAAI,IAAI;QACN,OAAO,mBAAmB,CAAC;IAC7B,CAAC;IACD,KAAK,CAAC,iBAAiB,CACrB,cAAsB,EACtB,UAA4B,EAAE;QAE9B,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACrE,CAAC;CACF,CAAC","sourcesContent":["import type {\n VideoInfoOptions,\n VideoInfoResult,\n} from \"./ExpoVideoMetadata.types\";\n\nexport default {\n get name(): string {\n return \"ExpoVideoMetadata\";\n },\n async getVideoInfoAsync(\n sourceFilename: string,\n options: VideoInfoOptions = {},\n ): Promise<VideoInfoResult> {\n throw new Error(\"ExpoVideoMetadata not supported on Expo Web yet\");\n },\n};\n"]}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { VideoInfoOptions, VideoInfoResult } from "./ExpoVideoMetadata.types";
|
|
2
|
+
export { VideoInfoOptions, VideoInfoResult };
|
|
3
|
+
/**
|
|
4
|
+
* Create an image thumbnail from video provided via `sourceFilename`.
|
|
5
|
+
*
|
|
6
|
+
* @param sourceFilename An URI of the video, local or remote.
|
|
7
|
+
* @param options Pass `headers` object in case `sourceFilename` is a remote URI, e.g { headers: "Authorization": "Bearer some-token" } etc.
|
|
8
|
+
*
|
|
9
|
+
* @return Returns a promise which fulfils with [`VideoInfoResult`](#Videoinforesult).
|
|
10
|
+
*/
|
|
11
|
+
export declare function getVideoInfoAsync(sourceFilename: string, options?: VideoInfoOptions): Promise<VideoInfoResult>;
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAG9E,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,CAAC;AAK7C;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,cAAc,EAAE,MAAM,EACtB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAE1B"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import ExpoVideoMetadataModule from "./ExpoVideoMetadataModule";
|
|
2
|
+
// Import the native module. On web, it will be resolved to ExpoVideoMetadata.web.ts
|
|
3
|
+
// and on native platforms to ExpoVideoMetadata.ts
|
|
4
|
+
/**
|
|
5
|
+
* Create an image thumbnail from video provided via `sourceFilename`.
|
|
6
|
+
*
|
|
7
|
+
* @param sourceFilename An URI of the video, local or remote.
|
|
8
|
+
* @param options Pass `headers` object in case `sourceFilename` is a remote URI, e.g { headers: "Authorization": "Bearer some-token" } etc.
|
|
9
|
+
*
|
|
10
|
+
* @return Returns a promise which fulfils with [`VideoInfoResult`](#Videoinforesult).
|
|
11
|
+
*/
|
|
12
|
+
export async function getVideoInfoAsync(sourceFilename, options = {}) {
|
|
13
|
+
return await ExpoVideoMetadataModule.getVideoInfo(sourceFilename, options);
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,uBAAuB,MAAM,2BAA2B,CAAC;AAIhE,oFAAoF;AACpF,kDAAkD;AAElD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,cAAsB,EACtB,UAA4B,EAAE;IAE9B,OAAO,MAAM,uBAAuB,CAAC,YAAY,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;AAC7E,CAAC","sourcesContent":["import { VideoInfoOptions, VideoInfoResult } from \"./ExpoVideoMetadata.types\";\nimport ExpoVideoMetadataModule from \"./ExpoVideoMetadataModule\";\n\nexport { VideoInfoOptions, VideoInfoResult };\n\n// Import the native module. On web, it will be resolved to ExpoVideoMetadata.web.ts\n// and on native platforms to ExpoVideoMetadata.ts\n\n/**\n * Create an image thumbnail from video provided via `sourceFilename`.\n *\n * @param sourceFilename An URI of the video, local or remote.\n * @param options Pass `headers` object in case `sourceFilename` is a remote URI, e.g { headers: \"Authorization\": \"Bearer some-token\" } etc.\n *\n * @return Returns a promise which fulfils with [`VideoInfoResult`](#Videoinforesult).\n */\nexport async function getVideoInfoAsync(\n sourceFilename: string,\n options: VideoInfoOptions = {},\n): Promise<VideoInfoResult> {\n return await ExpoVideoMetadataModule.getVideoInfo(sourceFilename, options);\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'ExpoVideoMetadata'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.homepage = package['homepage']
|
|
13
|
+
s.platform = :ios, '13.4'
|
|
14
|
+
s.swift_version = '5.4'
|
|
15
|
+
s.source = { git: 'https://github.com/hirbod/expo-video-metadata' }
|
|
16
|
+
s.static_framework = true
|
|
17
|
+
|
|
18
|
+
s.dependency 'ExpoModulesCore'
|
|
19
|
+
|
|
20
|
+
# Swift/Objective-C compatibility
|
|
21
|
+
s.pod_target_xcconfig = {
|
|
22
|
+
'DEFINES_MODULE' => 'YES',
|
|
23
|
+
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
s.source_files = "**/*.{h,m,swift}"
|
|
27
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import AVFoundation
|
|
3
|
+
|
|
4
|
+
public class ExpoVideoMetadataModule: Module {
|
|
5
|
+
public func definition() -> ModuleDefinition {
|
|
6
|
+
Name("ExpoVideoMetadata")
|
|
7
|
+
|
|
8
|
+
AsyncFunction("getVideoInfo", getVideoInfo).runOnQueue(.main)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
internal func getVideoInfo(sourceFilename: URL, options: ExpoVideoMetadataOptions) throws -> [String: Any] {
|
|
12
|
+
if sourceFilename.isFileURL {
|
|
13
|
+
guard FileSystemUtilities.permissions(appContext, for: sourceFilename).contains(.read) else {
|
|
14
|
+
throw FileSystemReadPermissionException(sourceFilename.absoluteString)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let asset = AVURLAsset.init(url: sourceFilename, options: ["AVURLAssetHTTPHeaderFieldsKey": options.headers])
|
|
19
|
+
let duration = CMTimeGetSeconds(asset.duration)
|
|
20
|
+
let hasAudio = asset.tracks(withMediaType: .audio).count > 0
|
|
21
|
+
|
|
22
|
+
var fileSize: Int64 = 0
|
|
23
|
+
if let fileAttributes = try? FileManager.default.attributesOfItem(atPath: sourceFilename.path),
|
|
24
|
+
let size = fileAttributes[.size] as? NSNumber {
|
|
25
|
+
fileSize = size.int64Value
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Initialize default values
|
|
29
|
+
var bitrate: Float = 0.0
|
|
30
|
+
var width: Int = 0
|
|
31
|
+
var height: Int = 0
|
|
32
|
+
var frameRate: Float = 0.0
|
|
33
|
+
var codec: String = ""
|
|
34
|
+
var orientation: String = ""
|
|
35
|
+
var audioSampleRate: Int = 0
|
|
36
|
+
var audioChannels: Int = 0
|
|
37
|
+
var audioCodec: String = ""
|
|
38
|
+
|
|
39
|
+
// If there are video tracks, extract more information
|
|
40
|
+
if let videoTrack = asset.tracks(withMediaType: .video).first {
|
|
41
|
+
// Bitrate
|
|
42
|
+
bitrate = videoTrack.estimatedDataRate
|
|
43
|
+
|
|
44
|
+
// Width and Height
|
|
45
|
+
let size = videoTrack.naturalSize
|
|
46
|
+
width = Int(size.width)
|
|
47
|
+
height = Int(size.height)
|
|
48
|
+
|
|
49
|
+
// Frame Rate
|
|
50
|
+
frameRate = videoTrack.nominalFrameRate
|
|
51
|
+
|
|
52
|
+
// Codec
|
|
53
|
+
if let firstFormatDescription = videoTrack.formatDescriptions.first {
|
|
54
|
+
let formatDescription = firstFormatDescription as! CMFormatDescription
|
|
55
|
+
let codecType = CMFormatDescriptionGetMediaSubType(formatDescription)
|
|
56
|
+
codec = fourCharCodeToString(fourCharCode: codecType)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Orientation
|
|
60
|
+
let transform = videoTrack.preferredTransform
|
|
61
|
+
if transform.a == 0 && transform.d == 0 {
|
|
62
|
+
orientation = (transform.b == 1.0) ? "Portrait" : "PortraitUpsideDown"
|
|
63
|
+
} else {
|
|
64
|
+
orientation = (transform.a == 1.0) ? "LandscapeRight" : "LandscapeLeft"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Audio track information
|
|
69
|
+
if let audioTrack = asset.tracks(withMediaType: .audio).first {
|
|
70
|
+
audioSampleRate = Int(audioTrack.naturalTimeScale)
|
|
71
|
+
|
|
72
|
+
// Extracting audio channels from the format descriptions
|
|
73
|
+
if let formatDescriptions = audioTrack.formatDescriptions as? [CMAudioFormatDescription],
|
|
74
|
+
let firstFormatDescription = formatDescriptions.first {
|
|
75
|
+
let audioStreamBasicDescription = CMAudioFormatDescriptionGetStreamBasicDescription(firstFormatDescription)?.pointee
|
|
76
|
+
audioChannels = Int(audioStreamBasicDescription?.mChannelsPerFrame ?? 0)
|
|
77
|
+
|
|
78
|
+
// Extract audio codec
|
|
79
|
+
let codecType = CMFormatDescriptionGetMediaSubType(firstFormatDescription)
|
|
80
|
+
audioCodec = fourCharCodeToString(fourCharCode: codecType)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return [
|
|
85
|
+
"duration": duration,
|
|
86
|
+
"hasAudio": hasAudio,
|
|
87
|
+
"fileSize": fileSize,
|
|
88
|
+
"bitrate": bitrate,
|
|
89
|
+
"fps": frameRate,
|
|
90
|
+
"width": width,
|
|
91
|
+
"height": height,
|
|
92
|
+
"codec": codec,
|
|
93
|
+
"orientation": orientation,
|
|
94
|
+
"audioSampleRate": audioSampleRate,
|
|
95
|
+
"audioChannels": audioChannels,
|
|
96
|
+
"audioCodec": audioCodec
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Helper function to convert FourCC code to String
|
|
101
|
+
private func fourCharCodeToString(fourCharCode: FourCharCode) -> String {
|
|
102
|
+
let characters = [
|
|
103
|
+
Character(UnicodeScalar((fourCharCode >> 24) & 0xFF)!),
|
|
104
|
+
Character(UnicodeScalar((fourCharCode >> 16) & 0xFF)!),
|
|
105
|
+
Character(UnicodeScalar((fourCharCode >> 8) & 0xFF)!),
|
|
106
|
+
Character(UnicodeScalar(fourCharCode & 0xFF)!)
|
|
107
|
+
]
|
|
108
|
+
return String(characters)
|
|
109
|
+
}
|
|
110
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "expo-video-metadata",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Module to retrieve informations from a video",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"types": "build/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "expo-module build",
|
|
9
|
+
"clean": "expo-module clean",
|
|
10
|
+
"lint": "expo-module lint",
|
|
11
|
+
"test": "expo-module test",
|
|
12
|
+
"prepare": "expo-module prepare",
|
|
13
|
+
"prepublishOnly": "expo-module prepublishOnly",
|
|
14
|
+
"expo-module": "expo-module",
|
|
15
|
+
"open:ios": "open -a \"Xcode\" example/ios",
|
|
16
|
+
"open:android": "open -a \"Android Studio\" example/android"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"react-native",
|
|
20
|
+
"expo",
|
|
21
|
+
"expo-video-metadata",
|
|
22
|
+
"ExpoVideoMetadata"
|
|
23
|
+
],
|
|
24
|
+
"repository": "https://github.com/hirbod/expo-video-metadata",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/hirbod/expo-video-metadata/issues"
|
|
27
|
+
},
|
|
28
|
+
"author": "Hirbod Mirjavadi <hm@nightstomp.com> (https://github.com/hirbod)",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"homepage": "https://github.com/hirbod/expo-video-metadata#readme",
|
|
31
|
+
"dependencies": {},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/react": "^18.0.25",
|
|
34
|
+
"expo-module-scripts": "^3.0.11",
|
|
35
|
+
"expo-modules-core": "^1.5.11"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"expo": "*",
|
|
39
|
+
"react": "*",
|
|
40
|
+
"react-native": "*"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type VideoInfoResult = {
|
|
2
|
+
/**
|
|
3
|
+
* Duration of the video in seconds (float).
|
|
4
|
+
*/
|
|
5
|
+
duration: number;
|
|
6
|
+
/**
|
|
7
|
+
* Tells if the video has a audio track. If the video has no audio track, its considered a mute video.
|
|
8
|
+
*/
|
|
9
|
+
hasAudio: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Width of the video in pixels.
|
|
12
|
+
*/
|
|
13
|
+
width: number;
|
|
14
|
+
/**
|
|
15
|
+
* Height of the video in pixels.
|
|
16
|
+
*/
|
|
17
|
+
height: number;
|
|
18
|
+
/**
|
|
19
|
+
* Frame rate of the video in frames per second.
|
|
20
|
+
*/
|
|
21
|
+
fps: number;
|
|
22
|
+
/**
|
|
23
|
+
* Bit rate of the video in bits per second.
|
|
24
|
+
*/
|
|
25
|
+
bitRate: number;
|
|
26
|
+
/**
|
|
27
|
+
* File size of the video in bytes. Works only for local files, returns 0 for remote files.
|
|
28
|
+
*/
|
|
29
|
+
fileSize: number;
|
|
30
|
+
/**
|
|
31
|
+
* Video codec.
|
|
32
|
+
*/
|
|
33
|
+
codec: string;
|
|
34
|
+
/**
|
|
35
|
+
* Video orientation.
|
|
36
|
+
*/
|
|
37
|
+
orientation:
|
|
38
|
+
| "Portrait"
|
|
39
|
+
| "PortraitUpsideDown"
|
|
40
|
+
| "LandscapeRight"
|
|
41
|
+
| "LandscapeLeft";
|
|
42
|
+
/**
|
|
43
|
+
* Audio sample rate of the video in samples per second.
|
|
44
|
+
*/
|
|
45
|
+
audioSampleRate: number;
|
|
46
|
+
/**
|
|
47
|
+
* Audio channel count of the video.
|
|
48
|
+
*/
|
|
49
|
+
audioChannels: number;
|
|
50
|
+
/**
|
|
51
|
+
* Audio codec of the video.
|
|
52
|
+
*/
|
|
53
|
+
audioCodec: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type VideoInfoOptions = {
|
|
57
|
+
/**
|
|
58
|
+
* In case `sourceFilename` is a remote URI, `headers` object is passed in a network request.
|
|
59
|
+
*/
|
|
60
|
+
headers?: Record<string, string>;
|
|
61
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
VideoInfoOptions,
|
|
3
|
+
VideoInfoResult,
|
|
4
|
+
} from "./ExpoVideoMetadata.types";
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
get name(): string {
|
|
8
|
+
return "ExpoVideoMetadata";
|
|
9
|
+
},
|
|
10
|
+
async getVideoInfoAsync(
|
|
11
|
+
sourceFilename: string,
|
|
12
|
+
options: VideoInfoOptions = {},
|
|
13
|
+
): Promise<VideoInfoResult> {
|
|
14
|
+
throw new Error("ExpoVideoMetadata not supported on Expo Web yet");
|
|
15
|
+
},
|
|
16
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { VideoInfoOptions, VideoInfoResult } from "./ExpoVideoMetadata.types";
|
|
2
|
+
import ExpoVideoMetadataModule from "./ExpoVideoMetadataModule";
|
|
3
|
+
|
|
4
|
+
export { VideoInfoOptions, VideoInfoResult };
|
|
5
|
+
|
|
6
|
+
// Import the native module. On web, it will be resolved to ExpoVideoMetadata.web.ts
|
|
7
|
+
// and on native platforms to ExpoVideoMetadata.ts
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create an image thumbnail from video provided via `sourceFilename`.
|
|
11
|
+
*
|
|
12
|
+
* @param sourceFilename An URI of the video, local or remote.
|
|
13
|
+
* @param options Pass `headers` object in case `sourceFilename` is a remote URI, e.g { headers: "Authorization": "Bearer some-token" } etc.
|
|
14
|
+
*
|
|
15
|
+
* @return Returns a promise which fulfils with [`VideoInfoResult`](#Videoinforesult).
|
|
16
|
+
*/
|
|
17
|
+
export async function getVideoInfoAsync(
|
|
18
|
+
sourceFilename: string,
|
|
19
|
+
options: VideoInfoOptions = {},
|
|
20
|
+
): Promise<VideoInfoResult> {
|
|
21
|
+
return await ExpoVideoMetadataModule.getVideoInfo(sourceFilename, options);
|
|
22
|
+
}
|