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.
Files changed (34) hide show
  1. package/.eslintrc.js +5 -0
  2. package/README.md +41 -0
  3. package/android/build.gradle +89 -0
  4. package/android/src/main/AndroidManifest.xml +2 -0
  5. package/android/src/main/java/expo/modules/videometadata/Exceptions.kt +9 -0
  6. package/android/src/main/java/expo/modules/videometadata/ExpoVideoMetadataModule.kt +203 -0
  7. package/android/src/main/java/expo/modules/videometadata/VideoMetadataOptions.kt +9 -0
  8. package/build/ExpoVideoMetadata.types.d.ts +57 -0
  9. package/build/ExpoVideoMetadata.types.d.ts.map +1 -0
  10. package/build/ExpoVideoMetadata.types.js +2 -0
  11. package/build/ExpoVideoMetadata.types.js.map +1 -0
  12. package/build/ExpoVideoMetadataModule.d.ts +3 -0
  13. package/build/ExpoVideoMetadataModule.d.ts.map +1 -0
  14. package/build/ExpoVideoMetadataModule.js +3 -0
  15. package/build/ExpoVideoMetadataModule.js.map +1 -0
  16. package/build/ExpoVideoMetadataModule.web.d.ts +7 -0
  17. package/build/ExpoVideoMetadataModule.web.d.ts.map +1 -0
  18. package/build/ExpoVideoMetadataModule.web.js +9 -0
  19. package/build/ExpoVideoMetadataModule.web.js.map +1 -0
  20. package/build/index.d.ts +12 -0
  21. package/build/index.d.ts.map +1 -0
  22. package/build/index.js +15 -0
  23. package/build/index.js.map +1 -0
  24. package/expo-module.config.json +9 -0
  25. package/ios/ExpoVideoMetadata.podspec +27 -0
  26. package/ios/ExpoVideoMetadataExceptions.swift +7 -0
  27. package/ios/ExpoVideoMetadataModule.swift +110 -0
  28. package/ios/ExpoVideoMetadataOptions.swift +5 -0
  29. package/package.json +42 -0
  30. package/src/ExpoVideoMetadata.types.ts +61 -0
  31. package/src/ExpoVideoMetadataModule.ts +2 -0
  32. package/src/ExpoVideoMetadataModule.web.ts +16 -0
  33. package/src/index.ts +22 -0
  34. package/tsconfig.json +9 -0
package/.eslintrc.js ADDED
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ root: true,
3
+ extends: ['universe/native', 'universe/web'],
4
+ ignorePatterns: ['build'],
5
+ };
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,2 @@
1
+ <manifest>
2
+ </manifest>
@@ -0,0 +1,9 @@
1
+ package expo.modules.videometadata
2
+
3
+ import expo.modules.kotlin.exception.CodedException
4
+
5
+ class VideoFileException :
6
+ CodedException("Can't read file")
7
+
8
+ class FilePermissionsModuleNotFound :
9
+ CodedException("File permissions module not found")
@@ -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,9 @@
1
+ package expo.modules.videometadata
2
+
3
+ import expo.modules.kotlin.records.Field
4
+ import expo.modules.kotlin.records.Record
5
+
6
+ data class ExpoVideoMetadataOptions(
7
+ @Field
8
+ val headers: Map<String, String> = emptyMap()
9
+ ) : Record
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ExpoVideoMetadata.types.js.map
@@ -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,3 @@
1
+ declare const _default: any;
2
+ export default _default;
3
+ //# sourceMappingURL=ExpoVideoMetadataModule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoVideoMetadataModule.d.ts","sourceRoot":"","sources":["../src/ExpoVideoMetadataModule.ts"],"names":[],"mappings":";AACA,wBAAwD"}
@@ -0,0 +1,3 @@
1
+ import { requireNativeModule } from "expo-modules-core";
2
+ export default requireNativeModule("ExpoVideoMetadata");
3
+ //# sourceMappingURL=ExpoVideoMetadataModule.js.map
@@ -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,9 @@
1
+ export default {
2
+ get name() {
3
+ return "ExpoVideoMetadata";
4
+ },
5
+ async getVideoInfoAsync(sourceFilename, options = {}) {
6
+ throw new Error("ExpoVideoMetadata not supported on Expo Web yet");
7
+ },
8
+ };
9
+ //# sourceMappingURL=ExpoVideoMetadataModule.web.js.map
@@ -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"]}
@@ -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,9 @@
1
+ {
2
+ "platforms": ["ios", "android", "web"],
3
+ "ios": {
4
+ "modules": ["ExpoVideoMetadataModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["expo.modules.videometadata.ExpoVideoMetadataModule"]
8
+ }
9
+ }
@@ -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,7 @@
1
+ import ExpoModulesCore
2
+
3
+ internal class FileSystemReadPermissionException: GenericException<String> {
4
+ override var reason: String {
5
+ "File '\(param)' is not readable"
6
+ }
7
+ }
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ import ExpoModulesCore
2
+
3
+ internal struct ExpoVideoMetadataOptions: Record {
4
+ @Field var headers: [String: String] = [String: String]()
5
+ }
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,2 @@
1
+ import { requireNativeModule } from "expo-modules-core";
2
+ export default requireNativeModule("ExpoVideoMetadata");
@@ -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
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ // @generated by expo-module-scripts
2
+ {
3
+ "extends": "expo-module-scripts/tsconfig.base",
4
+ "compilerOptions": {
5
+ "outDir": "./build"
6
+ },
7
+ "include": ["./src"],
8
+ "exclude": ["**/__mocks__/*", "**/__tests__/*"]
9
+ }