expo-video-metadata 1.3.1 → 1.5.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/.yarnrc.yml +3 -0
- package/android/src/main/java/expo/modules/videometadata/ExpoVideoMetadataModule.kt +45 -24
- package/build/ExpoVideoMetadata.types.d.ts +19 -0
- package/build/ExpoVideoMetadata.types.d.ts.map +1 -1
- package/build/ExpoVideoMetadata.types.js.map +1 -1
- package/build/ExpoVideoMetadataModule.js +1 -1
- package/build/ExpoVideoMetadataModule.js.map +1 -1
- package/build/ExpoVideoMetadataModule.web.d.ts +1 -0
- package/build/ExpoVideoMetadataModule.web.d.ts.map +1 -1
- package/build/ExpoVideoMetadataModule.web.js +45 -4
- package/build/ExpoVideoMetadataModule.web.js.map +1 -1
- package/ios/ExpoVideoMetadataModule.swift +53 -27
- package/package.json +6 -7
- package/src/ExpoVideoMetadata.types.ts +21 -0
- package/src/ExpoVideoMetadataModule.ts +1 -1
- package/src/ExpoVideoMetadataModule.web.ts +55 -8
package/.yarnrc.yml
ADDED
|
@@ -17,10 +17,10 @@ import kotlinx.coroutines.cancel
|
|
|
17
17
|
import kotlinx.coroutines.launch
|
|
18
18
|
import java.io.File
|
|
19
19
|
import android.media.MediaExtractor
|
|
20
|
-
import android.media.MediaFeature
|
|
21
20
|
import android.media.MediaFormat
|
|
22
21
|
import java.math.BigDecimal
|
|
23
22
|
import java.math.RoundingMode
|
|
23
|
+
import kotlin.math.abs
|
|
24
24
|
|
|
25
25
|
class ExpoVideoMetadataModule : Module() {
|
|
26
26
|
private val context
|
|
@@ -79,6 +79,9 @@ class ExpoVideoMetadataModule : Module() {
|
|
|
79
79
|
// Extract GPS location
|
|
80
80
|
val location = extractGPSLocation(retriever)
|
|
81
81
|
|
|
82
|
+
// get orientation
|
|
83
|
+
val orientation = getOrientation(rotation, width, height)
|
|
84
|
+
|
|
82
85
|
// release
|
|
83
86
|
retriever.release()
|
|
84
87
|
|
|
@@ -115,7 +118,6 @@ class ExpoVideoMetadataModule : Module() {
|
|
|
115
118
|
|
|
116
119
|
extractor.release()
|
|
117
120
|
|
|
118
|
-
|
|
119
121
|
promise.resolve(
|
|
120
122
|
mapOf(
|
|
121
123
|
"audioChannels" to audioChannels,
|
|
@@ -127,7 +129,16 @@ class ExpoVideoMetadataModule : Module() {
|
|
|
127
129
|
"hasAudio" to hasAudio,
|
|
128
130
|
"isHDR" to isHDR,
|
|
129
131
|
"audioCodec" to audioCodec,
|
|
130
|
-
"orientation" to
|
|
132
|
+
"orientation" to orientation,
|
|
133
|
+
"naturalOrientation" to if (width != null && height != null && width != 0 && height != 0) {
|
|
134
|
+
if (height > width) "Portrait" else "Landscape"
|
|
135
|
+
} else "Landscape",
|
|
136
|
+
"aspectRatio" to if (width != null && height != null && height != 0) {
|
|
137
|
+
width.toDouble() / height.toDouble()
|
|
138
|
+
} else null,
|
|
139
|
+
"is16_9" to if (width != null && height != null && height != 0) {
|
|
140
|
+
abs((width.toDouble() / height.toDouble()) - 16.0/9.0) < 0.01
|
|
141
|
+
} else false,
|
|
131
142
|
"audioSampleRate" to audioSampleRate,
|
|
132
143
|
"audioCodec" to audioCodec,
|
|
133
144
|
"codec" to videoCodec,
|
|
@@ -142,7 +153,6 @@ class ExpoVideoMetadataModule : Module() {
|
|
|
142
153
|
}
|
|
143
154
|
}
|
|
144
155
|
|
|
145
|
-
|
|
146
156
|
OnDestroy {
|
|
147
157
|
try {
|
|
148
158
|
moduleCoroutineScope.cancel(ModuleDestroyedException())
|
|
@@ -196,38 +206,50 @@ class ExpoVideoMetadataModule : Module() {
|
|
|
196
206
|
return permissionModuleInterface.getPathPermissions(context, url).contains(Permission.READ)
|
|
197
207
|
}
|
|
198
208
|
|
|
199
|
-
private fun getOrientation(rotation: Int?): String {
|
|
200
|
-
|
|
201
|
-
|
|
209
|
+
private fun getOrientation(rotation: Int?, width: Int?, height: Int?): String {
|
|
210
|
+
// If dimensions are null or zero, default to LandscapeRight
|
|
211
|
+
if (width == null || height == null || width == 0 || height == 0) {
|
|
212
|
+
return "LandscapeRight"
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
val isNaturallyPortrait = height > width
|
|
216
|
+
|
|
217
|
+
// If no rotation, use natural orientation
|
|
218
|
+
if (rotation == null) {
|
|
219
|
+
return if (isNaturallyPortrait) "Portrait" else "LandscapeRight"
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Normalize rotation to 0-360
|
|
223
|
+
val normalizedRotation = ((rotation % 360) + 360) % 360
|
|
224
|
+
|
|
225
|
+
return when (normalizedRotation) {
|
|
226
|
+
0 -> if (isNaturallyPortrait) "Portrait" else "LandscapeRight"
|
|
202
227
|
90 -> "Portrait"
|
|
203
|
-
180 -> "LandscapeLeft"
|
|
228
|
+
180 -> if (isNaturallyPortrait) "PortraitUpsideDown" else "LandscapeLeft"
|
|
204
229
|
270 -> "PortraitUpsideDown"
|
|
205
|
-
else -> "
|
|
230
|
+
else -> if (isNaturallyPortrait) "Portrait" else "LandscapeRight"
|
|
206
231
|
}
|
|
207
232
|
}
|
|
208
233
|
|
|
209
|
-
|
|
210
234
|
private fun mapMimeTypeToCodecName(mimeType: String): String {
|
|
211
235
|
return when {
|
|
212
236
|
mimeType.startsWith("audio/") -> {
|
|
213
237
|
when {
|
|
214
|
-
mimeType.contains("mp4a-latm") -> "aac"
|
|
215
|
-
mimeType.contains("ac3") -> "ac3"
|
|
216
|
-
mimeType.contains("opus") -> "opus"
|
|
217
|
-
mimeType.contains("vorbis") -> "vorbis"
|
|
218
|
-
mimeType.contains("flac") -> "flac"
|
|
219
|
-
// Add more audio mappings as needed
|
|
238
|
+
mimeType.contains("mp4a-latm") -> "aac"
|
|
239
|
+
mimeType.contains("ac3") -> "ac3"
|
|
240
|
+
mimeType.contains("opus") -> "opus"
|
|
241
|
+
mimeType.contains("vorbis") -> "vorbis"
|
|
242
|
+
mimeType.contains("flac") -> "flac"
|
|
220
243
|
else -> mimeType.substringAfter("audio/")
|
|
221
244
|
}
|
|
222
245
|
}
|
|
223
246
|
mimeType.startsWith("video/") -> {
|
|
224
247
|
when {
|
|
225
|
-
mimeType.contains("avc") || mimeType.contains("h264") -> "avc1"
|
|
226
|
-
mimeType.contains("hev") || mimeType.contains("h265") -> "hev1"
|
|
227
|
-
mimeType.contains("vp9") -> "vp9"
|
|
228
|
-
mimeType.contains("vp8") -> "vp8"
|
|
229
|
-
mimeType.contains("mp4v-es") -> "mp4v"
|
|
230
|
-
// Add more video mappings as needed
|
|
248
|
+
mimeType.contains("avc") || mimeType.contains("h264") -> "avc1"
|
|
249
|
+
mimeType.contains("hev") || mimeType.contains("h265") -> "hev1"
|
|
250
|
+
mimeType.contains("vp9") -> "vp9"
|
|
251
|
+
mimeType.contains("vp8") -> "vp8"
|
|
252
|
+
mimeType.contains("mp4v-es") -> "mp4v"
|
|
231
253
|
else -> mimeType.substringAfter("video/")
|
|
232
254
|
}
|
|
233
255
|
}
|
|
@@ -235,7 +257,6 @@ class ExpoVideoMetadataModule : Module() {
|
|
|
235
257
|
}
|
|
236
258
|
}
|
|
237
259
|
|
|
238
|
-
|
|
239
260
|
private inline fun withModuleScope(promise: Promise, crossinline block: () -> Unit) = moduleCoroutineScope.launch {
|
|
240
261
|
try {
|
|
241
262
|
block()
|
|
@@ -250,4 +271,4 @@ class ExpoVideoMetadataModule : Module() {
|
|
|
250
271
|
private const val TAG = "ExpoVideoMetadata"
|
|
251
272
|
private const val ERROR_TAG = "E_VIDEO_METADATA"
|
|
252
273
|
}
|
|
253
|
-
}
|
|
274
|
+
}
|
|
@@ -43,8 +43,27 @@ export type VideoInfoResult = {
|
|
|
43
43
|
/**
|
|
44
44
|
* Video orientation.
|
|
45
45
|
* Supported on all platforms, but on Web it may return an empty string.
|
|
46
|
+
* Orientation takes into account both the natural dimensions AND any rotation/transform applied to the video:
|
|
47
|
+
* - Portrait: The video is in portrait mode.
|
|
48
|
+
* - PortraitUpsideDown: The video is in portrait mode, but upside down.
|
|
49
|
+
* - Landscape: The video is in landscape mode.
|
|
50
|
+
* - LandscapeRight: The video is in landscape mode, but rotated 90 degrees clockwise.
|
|
51
|
+
* - LandscapeLeft: The video is in landscape mode, but rotated 90 degrees counter-clockwise.
|
|
46
52
|
*/
|
|
47
53
|
orientation: "Portrait" | "PortraitUpsideDown" | "Landscape" | "LandscapeRight" | "LandscapeLeft";
|
|
54
|
+
/**
|
|
55
|
+
* Natural orientation of the video.
|
|
56
|
+
* This is the orientation of the video as it was recorded, without any rotation/transform applied.
|
|
57
|
+
*/
|
|
58
|
+
naturalOrientation: "Portrait" | "Landscape";
|
|
59
|
+
/**
|
|
60
|
+
* Aspect ratio of the video.
|
|
61
|
+
*/
|
|
62
|
+
aspectRatio: number;
|
|
63
|
+
/**
|
|
64
|
+
* Tells if the video is 16:9.
|
|
65
|
+
*/
|
|
66
|
+
is16_9: boolean;
|
|
48
67
|
/**
|
|
49
68
|
* Audio sample rate of the video in samples per second.
|
|
50
69
|
*/
|
|
@@ -1 +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;;;OAGG;IACH,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IACtB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd
|
|
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;;;OAGG;IACH,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IACtB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;OASG;IACH,WAAW,EACP,UAAU,GACV,oBAAoB,GACpB,WAAW,GACX,gBAAgB,GAChB,eAAe,CAAC;IACpB;;;OAGG;IACH,kBAAkB,EACd,UAAU,GACV,WAAW,CAAC;IAChB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC;IAChB;;OAEG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,QAAQ,EAAE;QACR,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,IAAI,CAAC;CACV,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;AAE/C,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,CAAC"}
|
|
@@ -1 +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 * Available only on iOS >= 14 and Android. Tells if the video is a HDR video.\n * Will return null if it could not be determined. (e.g. on Web or on older iOS/Android versions)\n */\n isHDR: boolean | null;\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 * Works on iOS, Android and Web (except Safari).\n */\n fps: number;\n /**\n * Bit rate of the video in bits per second.\n * Supported on all platforms.\n */\n bitRate: number;\n /**\n * File size of the video in bytes. Works only for local files, returns 0 for remote files.\n * Supported on all platforms.\n */\n fileSize: number;\n /**\n * Video codec.\n * Supported on all platforms, but on Web it may return an empty string.\n */\n codec: string;\n /**\n * Video orientation.\n * Supported on all platforms, but on Web it may return an empty string.\n */\n orientation:\n | \"Portrait\"\n | \"PortraitUpsideDown\"\n | \"Landscape\"\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 * Location where the video was recorded.\n * Supported on iOS and Android (if the video contains location metadata)\n */\n location: {\n latitude: number;\n longitude: number;\n altitude?: number;\n } | null;\n};\n\nexport type VideoSource = string | File | Blob;\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"]}
|
|
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 * Available only on iOS >= 14 and Android. Tells if the video is a HDR video.\n * Will return null if it could not be determined. (e.g. on Web or on older iOS/Android versions)\n */\n isHDR: boolean | null;\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 * Works on iOS, Android and Web (except Safari).\n */\n fps: number;\n /**\n * Bit rate of the video in bits per second.\n * Supported on all platforms.\n */\n bitRate: number;\n /**\n * File size of the video in bytes. Works only for local files, returns 0 for remote files.\n * Supported on all platforms.\n */\n fileSize: number;\n /**\n * Video codec.\n * Supported on all platforms, but on Web it may return an empty string.\n */\n codec: string;\n /**\n * Video orientation.\n * Supported on all platforms, but on Web it may return an empty string.\n * Orientation takes into account both the natural dimensions AND any rotation/transform applied to the video:\n * - Portrait: The video is in portrait mode.\n * - PortraitUpsideDown: The video is in portrait mode, but upside down.\n * - Landscape: The video is in landscape mode.\n * - LandscapeRight: The video is in landscape mode, but rotated 90 degrees clockwise.\n * - LandscapeLeft: The video is in landscape mode, but rotated 90 degrees counter-clockwise.\n */\n orientation:\n | \"Portrait\"\n | \"PortraitUpsideDown\"\n | \"Landscape\"\n | \"LandscapeRight\"\n | \"LandscapeLeft\";\n /**\n * Natural orientation of the video.\n * This is the orientation of the video as it was recorded, without any rotation/transform applied.\n */\n naturalOrientation:\n | \"Portrait\"\n | \"Landscape\";\n /**\n * Aspect ratio of the video.\n */\n aspectRatio: number;\n /**\n * Tells if the video is 16:9.\n */\n is16_9: boolean;\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 * Location where the video was recorded.\n * Supported on iOS and Android (if the video contains location metadata)\n */\n location: {\n latitude: number;\n longitude: number;\n altitude?: number;\n } | null;\n};\n\nexport type VideoSource = string | File | Blob;\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"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoVideoMetadataModule.js","sourceRoot":"","sources":["../src/ExpoVideoMetadataModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"ExpoVideoMetadataModule.js","sourceRoot":"","sources":["../src/ExpoVideoMetadataModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAC3C,eAAe,mBAAmB,CAAC,mBAAmB,CAAC,CAAC","sourcesContent":["import { requireNativeModule } from \"expo\";\nexport default requireNativeModule(\"ExpoVideoMetadata\");\n"]}
|
|
@@ -21,6 +21,7 @@ interface HTMLVideoElementWithTracks extends HTMLVideoElement {
|
|
|
21
21
|
}
|
|
22
22
|
declare const _default: {
|
|
23
23
|
name: string;
|
|
24
|
+
getVideoOrientation(video: HTMLVideoElementWithTracks): "Portrait" | "PortraitUpsideDown" | "LandscapeRight" | "LandscapeLeft";
|
|
24
25
|
getVideoFrameRate(videoElement: HTMLVideoElementWithTracks): number;
|
|
25
26
|
getAudioBuffer(audioUrl: string, options?: RequestInit): Promise<{
|
|
26
27
|
sampleRate: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoVideoMetadataModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoVideoMetadataModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EACf,WAAW,EACZ,MAAM,2BAA2B,CAAC;AAEnC,UAAU,KAAK;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,UAAW,SAAQ,KAAK;IAChC,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,UAAU,UAAW,SAAQ,KAAK;IAChC,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;AAExB,UAAU,0BAA2B,SAAQ,gBAAgB;IAC3D,WAAW,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,aAAa,CAAC,IAAI,WAAW,CAAC;CAC/B
|
|
1
|
+
{"version":3,"file":"ExpoVideoMetadataModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoVideoMetadataModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EACf,WAAW,EACZ,MAAM,2BAA2B,CAAC;AAEnC,UAAU,KAAK;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,UAAW,SAAQ,KAAK;IAChC,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,UAAU,UAAW,SAAQ,KAAK;IAChC,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;AAExB,UAAU,0BAA2B,SAAQ,gBAAgB;IAC3D,WAAW,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,aAAa,CAAC,IAAI,WAAW,CAAC;CAC/B;;;+BAK4B,0BAA0B;oCA2CrB,0BAA0B;6BAkB9C,MAAM,YACP,WAAW,GACnB,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,CAAC;oCA0B5B,MAAM,GAAG,MAAM;qBAKxB,MAAM,YAAW,WAAW,GAAQ,OAAO,CAAC,MAAM,CAAC;yBAgBhE,WAAW,YACV,gBAAgB,GACxB,OAAO,CAAC,eAAe,CAAC;;AAnH7B,wBA2ME"}
|
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
export default {
|
|
2
2
|
name: "ExpoVideoMetadata",
|
|
3
|
+
getVideoOrientation(video) {
|
|
4
|
+
// Get the video element's transform style
|
|
5
|
+
const transform = window.getComputedStyle(video).transform;
|
|
6
|
+
const matrix = new DOMMatrix(transform);
|
|
7
|
+
// Calculate rotation angle from transform matrix
|
|
8
|
+
const rotation = Math.round(Math.atan2(matrix.b, matrix.a) * (180 / Math.PI));
|
|
9
|
+
// Get natural dimensions
|
|
10
|
+
const { videoWidth: width, videoHeight: height } = video;
|
|
11
|
+
const isNaturallyPortrait = height > width;
|
|
12
|
+
// First check if there's rotation applied via CSS transform
|
|
13
|
+
if (rotation !== 0) {
|
|
14
|
+
switch ((rotation + 360) % 360) {
|
|
15
|
+
case 90:
|
|
16
|
+
return "Portrait";
|
|
17
|
+
case 270:
|
|
18
|
+
return "PortraitUpsideDown";
|
|
19
|
+
case 0:
|
|
20
|
+
return isNaturallyPortrait ? "Portrait" : "LandscapeRight";
|
|
21
|
+
case 180:
|
|
22
|
+
return isNaturallyPortrait ? "PortraitUpsideDown" : "LandscapeLeft";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// If no rotation, use natural dimensions
|
|
26
|
+
// Check for exact 16:9 ratio to handle the special case
|
|
27
|
+
const aspectRatio = width / height;
|
|
28
|
+
const is16_9 = Math.abs(aspectRatio - 16 / 9) < 0.01;
|
|
29
|
+
if (is16_9) {
|
|
30
|
+
// For 16:9 videos, try to detect orientation from video track data
|
|
31
|
+
const videoTrack = video.videoTracks?.[0];
|
|
32
|
+
if (videoTrack?.label?.toLowerCase().includes('portrait')) {
|
|
33
|
+
return "Portrait";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Default to dimensions-based orientation
|
|
37
|
+
return isNaturallyPortrait ? "Portrait" : "LandscapeRight";
|
|
38
|
+
},
|
|
3
39
|
getVideoFrameRate(videoElement) {
|
|
4
40
|
if (!videoElement.captureStream) {
|
|
5
41
|
console.info("captureStream method not supported");
|
|
@@ -28,7 +64,6 @@ export default {
|
|
|
28
64
|
}
|
|
29
65
|
catch (decodeError) {
|
|
30
66
|
console.info("Error decoding audio data (no audio?):", decodeError);
|
|
31
|
-
// If decoding fails, assume no audio
|
|
32
67
|
return { sampleRate: 0, numberOfChannels: 0 };
|
|
33
68
|
}
|
|
34
69
|
finally {
|
|
@@ -37,7 +72,6 @@ export default {
|
|
|
37
72
|
}
|
|
38
73
|
catch (error) {
|
|
39
74
|
console.info("Error decoding audio data (CORS?):", error);
|
|
40
|
-
// If any error occurs during the process, return default values silently
|
|
41
75
|
return { sampleRate: 0, numberOfChannels: 0 };
|
|
42
76
|
}
|
|
43
77
|
},
|
|
@@ -103,8 +137,12 @@ export default {
|
|
|
103
137
|
? source.size
|
|
104
138
|
: await this.getFileSize(videoUrl, options);
|
|
105
139
|
const bitRate = fileSize && duration ? Math.floor(fileSize / duration) : 0;
|
|
106
|
-
const { numberOfChannels: audioChannels, sampleRate: audioSampleRate } = await this.getAudioBuffer(videoUrl);
|
|
140
|
+
const { numberOfChannels: audioChannels, sampleRate: audioSampleRate } = await this.getAudioBuffer(videoUrl, options);
|
|
107
141
|
const fps = this.getVideoFrameRate(video);
|
|
142
|
+
const orientation = this.getVideoOrientation(video);
|
|
143
|
+
// Calculate additional metadata
|
|
144
|
+
const aspectRatio = width / height;
|
|
145
|
+
const is16_9 = Math.abs(aspectRatio - 16 / 9) < 0.01;
|
|
108
146
|
return {
|
|
109
147
|
duration,
|
|
110
148
|
width,
|
|
@@ -118,7 +156,10 @@ export default {
|
|
|
118
156
|
codec: videoTrack?.label ?? "",
|
|
119
157
|
audioChannels,
|
|
120
158
|
fps,
|
|
121
|
-
orientation
|
|
159
|
+
orientation,
|
|
160
|
+
aspectRatio,
|
|
161
|
+
is16_9,
|
|
162
|
+
naturalOrientation: width >= height ? "Landscape" : "Portrait",
|
|
122
163
|
location: null, // not supported on web
|
|
123
164
|
};
|
|
124
165
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoVideoMetadataModule.web.js","sourceRoot":"","sources":["../src/ExpoVideoMetadataModule.web.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n VideoInfoOptions,\n VideoInfoResult,\n VideoSource,\n} from \"./ExpoVideoMetadata.types\";\n\ninterface Track {\n id: string;\n kind: string;\n label: string;\n language: string;\n}\n\ninterface AudioTrack extends Track {\n enabled: boolean;\n}\n\ninterface VideoTrack extends Track {\n selected: boolean;\n}\n\ntype TrackList<T> = T[];\n\ninterface HTMLVideoElementWithTracks extends HTMLVideoElement {\n videoTracks?: TrackList<VideoTrack>;\n audioTracks?: TrackList<AudioTrack>;\n mozHasAudio?: boolean;\n webkitAudioDecodedByteCount?: number;\n captureStream?(): MediaStream;\n}\n\nexport default {\n name: \"ExpoVideoMetadata\",\n\n getVideoFrameRate(videoElement: HTMLVideoElementWithTracks) {\n if (!videoElement.captureStream) {\n console.info(\"captureStream method not supported\");\n return 0;\n }\n\n const stream = videoElement.captureStream();\n const [videoTrack] = stream.getVideoTracks();\n\n if (!videoTrack) {\n console.info(\"No video track found\");\n return 0;\n }\n\n return videoTrack.getSettings().frameRate ?? 0;\n },\n\n async getAudioBuffer(\n audioUrl: string,\n options: RequestInit = {}\n ): Promise<{ sampleRate: number; numberOfChannels: number }> {\n try {\n const response = await fetch(audioUrl, options);\n const arrayBuffer = await response.arrayBuffer();\n\n const audioContext = new (window.AudioContext ||\n (window as any).webkitAudioContext)();\n\n try {\n const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n return {\n sampleRate: audioBuffer.sampleRate,\n numberOfChannels: audioBuffer.numberOfChannels,\n };\n } catch (decodeError) {\n console.info(\"Error decoding audio data (no audio?):\", decodeError);\n // If decoding fails, assume no audio\n return { sampleRate: 0, numberOfChannels: 0 };\n } finally {\n await audioContext.close();\n }\n } catch (error) {\n console.info(\"Error decoding audio data (CORS?):\", error);\n\n // If any error occurs during the process, return default values silently\n return { sampleRate: 0, numberOfChannels: 0 };\n }\n },\n\n getBase64FileSize(base64String: string): number {\n const base64Data = base64String.replace(/^data:.+;base64,/, \"\");\n return atob(base64Data).length;\n },\n\n async getFileSize(url: string, options: RequestInit = {}): Promise<number> {\n if (url.startsWith(\"data:\")) {\n return this.getBase64FileSize(url);\n }\n\n try {\n const response = await fetch(url, { method: \"HEAD\", ...options });\n const contentLength = response.headers.get(\"Content-Length\");\n return contentLength ? parseInt(contentLength, 10) : 0;\n } catch (error) {\n console.info(\"Error fetching file size for URL (CORS?):\", url, error);\n return 0;\n }\n },\n\n async getVideoInfo(\n source: VideoSource,\n options: VideoInfoOptions = {}\n ): Promise<VideoInfoResult> {\n const video = document.createElement(\"video\") as HTMLVideoElementWithTracks;\n let videoUrl = \"\";\n\n if (typeof source === \"string\") {\n videoUrl = source;\n } else if (source instanceof File || source instanceof Blob) {\n videoUrl = URL.createObjectURL(source);\n }\n\n Object.assign(video, {\n crossOrigin: \"anonymous\",\n autoplay: true,\n muted: true,\n playsInline: true,\n });\n\n const resetVideo = () => {\n video.pause();\n video.removeAttribute(\"src\");\n video.load();\n video.remove();\n // Revoke the object URL if it was created\n if (source instanceof File || source instanceof Blob) {\n URL.revokeObjectURL(videoUrl);\n }\n };\n\n try {\n await new Promise<void>((resolve, reject) => {\n // Can't use `loadedmetadata` event because it does not contain videoTracks, audioTracks and other metadata\n video.onloadeddata = () => resolve();\n video.onerror = () =>\n reject(new Error(\"Failed to load video metadata\"));\n video.src = videoUrl;\n video.load();\n video.pause();\n });\n\n const { duration, videoWidth: width, videoHeight: height } = video;\n const videoTrack = video.videoTracks?.[0];\n const audioTrack = video.audioTracks?.[0];\n\n const hasAudio =\n Boolean(video.audioTracks?.length) ||\n video.mozHasAudio ||\n Boolean(video.webkitAudioDecodedByteCount);\n\n const fileSize =\n source instanceof File || source instanceof Blob\n ? source.size\n : await this.getFileSize(videoUrl, options);\n\n const bitRate =\n fileSize && duration ? Math.floor(fileSize / duration) : 0;\n\n const { numberOfChannels: audioChannels, sampleRate: audioSampleRate } =\n await this.getAudioBuffer(videoUrl);\n\n const fps = this.getVideoFrameRate(video);\n\n return {\n duration,\n width,\n height,\n bitRate,\n fileSize,\n hasAudio,\n audioSampleRate,\n isHDR: null, // not supported on web\n audioCodec: audioTrack?.label ?? \"\",\n codec: videoTrack?.label ?? \"\",\n audioChannels,\n fps,\n orientation: width >= height ? \"Landscape\" : \"Portrait\",\n location: null, // not supported on web\n };\n } finally {\n resetVideo();\n }\n },\n};\n"]}
|
|
1
|
+
{"version":3,"file":"ExpoVideoMetadataModule.web.js","sourceRoot":"","sources":["../src/ExpoVideoMetadataModule.web.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n VideoInfoOptions,\n VideoInfoResult,\n VideoSource,\n} from \"./ExpoVideoMetadata.types\";\n\ninterface Track {\n id: string;\n kind: string;\n label: string;\n language: string;\n}\n\ninterface AudioTrack extends Track {\n enabled: boolean;\n}\n\ninterface VideoTrack extends Track {\n selected: boolean;\n}\n\ntype TrackList<T> = T[];\n\ninterface HTMLVideoElementWithTracks extends HTMLVideoElement {\n videoTracks?: TrackList<VideoTrack>;\n audioTracks?: TrackList<AudioTrack>;\n mozHasAudio?: boolean;\n webkitAudioDecodedByteCount?: number;\n captureStream?(): MediaStream;\n}\n\nexport default {\n name: \"ExpoVideoMetadata\",\n\n getVideoOrientation(video: HTMLVideoElementWithTracks) {\n // Get the video element's transform style\n const transform = window.getComputedStyle(video).transform;\n const matrix = new DOMMatrix(transform);\n\n // Calculate rotation angle from transform matrix\n const rotation = Math.round(Math.atan2(matrix.b, matrix.a) * (180 / Math.PI));\n\n // Get natural dimensions\n const { videoWidth: width, videoHeight: height } = video;\n const isNaturallyPortrait = height > width;\n\n // First check if there's rotation applied via CSS transform\n if (rotation !== 0) {\n switch ((rotation + 360) % 360) {\n case 90:\n return \"Portrait\";\n case 270:\n return \"PortraitUpsideDown\";\n case 0:\n return isNaturallyPortrait ? \"Portrait\" : \"LandscapeRight\";\n case 180:\n return isNaturallyPortrait ? \"PortraitUpsideDown\" : \"LandscapeLeft\";\n }\n }\n\n // If no rotation, use natural dimensions\n // Check for exact 16:9 ratio to handle the special case\n const aspectRatio = width / height;\n const is16_9 = Math.abs(aspectRatio - 16/9) < 0.01;\n\n if (is16_9) {\n // For 16:9 videos, try to detect orientation from video track data\n const videoTrack = video.videoTracks?.[0];\n if (videoTrack?.label?.toLowerCase().includes('portrait')) {\n return \"Portrait\";\n }\n }\n\n // Default to dimensions-based orientation\n return isNaturallyPortrait ? \"Portrait\" : \"LandscapeRight\";\n },\n\n getVideoFrameRate(videoElement: HTMLVideoElementWithTracks) {\n if (!videoElement.captureStream) {\n console.info(\"captureStream method not supported\");\n return 0;\n }\n\n const stream = videoElement.captureStream();\n const [videoTrack] = stream.getVideoTracks();\n\n if (!videoTrack) {\n console.info(\"No video track found\");\n return 0;\n }\n\n return videoTrack.getSettings().frameRate ?? 0;\n },\n\n async getAudioBuffer(\n audioUrl: string,\n options: RequestInit = {}\n ): Promise<{ sampleRate: number; numberOfChannels: number }> {\n try {\n const response = await fetch(audioUrl, options);\n const arrayBuffer = await response.arrayBuffer();\n\n const audioContext = new (window.AudioContext ||\n (window as any).webkitAudioContext)();\n\n try {\n const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n return {\n sampleRate: audioBuffer.sampleRate,\n numberOfChannels: audioBuffer.numberOfChannels,\n };\n } catch (decodeError) {\n console.info(\"Error decoding audio data (no audio?):\", decodeError);\n return { sampleRate: 0, numberOfChannels: 0 };\n } finally {\n await audioContext.close();\n }\n } catch (error) {\n console.info(\"Error decoding audio data (CORS?):\", error);\n return { sampleRate: 0, numberOfChannels: 0 };\n }\n },\n\n getBase64FileSize(base64String: string): number {\n const base64Data = base64String.replace(/^data:.+;base64,/, \"\");\n return atob(base64Data).length;\n },\n\n async getFileSize(url: string, options: RequestInit = {}): Promise<number> {\n if (url.startsWith(\"data:\")) {\n return this.getBase64FileSize(url);\n }\n\n try {\n const response = await fetch(url, { method: \"HEAD\", ...options });\n const contentLength = response.headers.get(\"Content-Length\");\n return contentLength ? parseInt(contentLength, 10) : 0;\n } catch (error) {\n console.info(\"Error fetching file size for URL (CORS?):\", url, error);\n return 0;\n }\n },\n\n async getVideoInfo(\n source: VideoSource,\n options: VideoInfoOptions = {}\n ): Promise<VideoInfoResult> {\n const video = document.createElement(\"video\") as HTMLVideoElementWithTracks;\n let videoUrl = \"\";\n\n if (typeof source === \"string\") {\n videoUrl = source;\n } else if (source instanceof File || source instanceof Blob) {\n videoUrl = URL.createObjectURL(source);\n }\n\n Object.assign(video, {\n crossOrigin: \"anonymous\",\n autoplay: true,\n muted: true,\n playsInline: true,\n });\n\n const resetVideo = () => {\n video.pause();\n video.removeAttribute(\"src\");\n video.load();\n video.remove();\n // Revoke the object URL if it was created\n if (source instanceof File || source instanceof Blob) {\n URL.revokeObjectURL(videoUrl);\n }\n };\n\n try {\n await new Promise<void>((resolve, reject) => {\n // Can't use `loadedmetadata` event because it does not contain videoTracks, audioTracks and other metadata\n video.onloadeddata = () => resolve();\n video.onerror = () => reject(new Error(\"Failed to load video metadata\"));\n video.src = videoUrl;\n video.load();\n video.pause();\n });\n\n const { duration, videoWidth: width, videoHeight: height } = video;\n const videoTrack = video.videoTracks?.[0];\n const audioTrack = video.audioTracks?.[0];\n\n const hasAudio =\n Boolean(video.audioTracks?.length) ||\n video.mozHasAudio ||\n Boolean(video.webkitAudioDecodedByteCount);\n\n const fileSize =\n source instanceof File || source instanceof Blob\n ? source.size\n : await this.getFileSize(videoUrl, options);\n\n const bitRate =\n fileSize && duration ? Math.floor(fileSize / duration) : 0;\n\n const { numberOfChannels: audioChannels, sampleRate: audioSampleRate } =\n await this.getAudioBuffer(videoUrl, options);\n\n const fps = this.getVideoFrameRate(video);\n const orientation = this.getVideoOrientation(video);\n\n // Calculate additional metadata\n const aspectRatio = width / height;\n const is16_9 = Math.abs(aspectRatio - 16/9) < 0.01;\n\n return {\n duration,\n width,\n height,\n bitRate,\n fileSize,\n hasAudio,\n audioSampleRate,\n isHDR: null, // not supported on web\n audioCodec: audioTrack?.label ?? \"\",\n codec: videoTrack?.label ?? \"\",\n audioChannels,\n fps,\n orientation,\n aspectRatio,\n is16_9,\n naturalOrientation: width >= height ? \"Landscape\" : \"Portrait\",\n location: null, // not supported on web\n };\n } finally {\n resetVideo();\n }\n },\n};"]}
|
|
@@ -4,27 +4,55 @@ import AVFoundation
|
|
|
4
4
|
public class ExpoVideoMetadataModule: Module {
|
|
5
5
|
public func definition() -> ModuleDefinition {
|
|
6
6
|
Name("ExpoVideoMetadata")
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
AsyncFunction("getVideoInfo", getVideoInfo)
|
|
9
9
|
}
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
private func getOrientation(from videoTrack: AVAssetTrack) -> String {
|
|
12
|
+
let transform = videoTrack.preferredTransform
|
|
13
|
+
let size = videoTrack.naturalSize
|
|
14
|
+
|
|
15
|
+
// First check natural dimensions
|
|
16
|
+
let isNaturallyPortrait = size.height > size.width
|
|
17
|
+
|
|
18
|
+
// Calculate rotation angle from transform
|
|
19
|
+
let angle = atan2(transform.b, transform.a)
|
|
20
|
+
let degrees = angle * 180 / .pi
|
|
21
|
+
let rotation = (Int(round(degrees)) + 360) % 360
|
|
22
|
+
|
|
23
|
+
// Combine transform rotation with natural orientation
|
|
24
|
+
switch rotation {
|
|
25
|
+
case 0:
|
|
26
|
+
return isNaturallyPortrait ? "Portrait" : "LandscapeRight"
|
|
27
|
+
case 90, -270:
|
|
28
|
+
return "Portrait"
|
|
29
|
+
case 180, -180:
|
|
30
|
+
return isNaturallyPortrait ? "PortraitUpsideDown" : "LandscapeLeft"
|
|
31
|
+
case 270, -90:
|
|
32
|
+
return "PortraitUpsideDown"
|
|
33
|
+
default:
|
|
34
|
+
// For unknown rotations, use natural dimensions
|
|
35
|
+
return isNaturallyPortrait ? "Portrait" : "LandscapeRight"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
11
39
|
internal func getVideoInfo(sourceFilename: URL, options: ExpoVideoMetadataOptions) throws -> [String: Any] {
|
|
12
40
|
if sourceFilename.isFileURL {
|
|
13
41
|
guard FileSystemUtilities.permissions(appContext, for: sourceFilename).contains(.read) else {
|
|
14
42
|
throw FileSystemReadPermissionException(sourceFilename.absoluteString)
|
|
15
43
|
}
|
|
16
44
|
}
|
|
17
|
-
|
|
45
|
+
|
|
18
46
|
let asset = AVURLAsset.init(url: sourceFilename, options: ["AVURLAssetHTTPHeaderFieldsKey": options.headers])
|
|
19
47
|
let duration = CMTimeGetSeconds(asset.duration)
|
|
20
48
|
let hasAudio = asset.tracks(withMediaType: .audio).count > 0
|
|
21
|
-
|
|
49
|
+
|
|
22
50
|
var fileSize: Int64 = 0
|
|
23
51
|
if let fileAttributes = try? FileManager.default.attributesOfItem(atPath: sourceFilename.path),
|
|
24
52
|
let size = fileAttributes[.size] as? NSNumber {
|
|
25
53
|
fileSize = size.int64Value
|
|
26
54
|
}
|
|
27
|
-
|
|
55
|
+
|
|
28
56
|
// Initialize default values
|
|
29
57
|
var bitrate: Float = 0.0
|
|
30
58
|
var width: Int = 0
|
|
@@ -37,57 +65,52 @@ public class ExpoVideoMetadataModule: Module {
|
|
|
37
65
|
var audioChannels: Int = 0
|
|
38
66
|
var audioCodec: String = ""
|
|
39
67
|
var location: [String: Double]? = nil
|
|
40
|
-
|
|
68
|
+
|
|
41
69
|
// If there are video tracks, extract more information
|
|
42
70
|
if let videoTrack = asset.tracks(withMediaType: .video).first {
|
|
43
71
|
// Bitrate
|
|
44
72
|
bitrate = videoTrack.estimatedDataRate
|
|
45
|
-
|
|
73
|
+
|
|
46
74
|
// Width and Height
|
|
47
75
|
let size = videoTrack.naturalSize
|
|
48
76
|
width = Int(size.width)
|
|
49
77
|
height = Int(size.height)
|
|
50
|
-
|
|
78
|
+
|
|
51
79
|
// Frame Rate
|
|
52
80
|
frameRate = videoTrack.nominalFrameRate
|
|
53
|
-
|
|
81
|
+
|
|
54
82
|
// Codec
|
|
55
83
|
if let firstFormatDescription = videoTrack.formatDescriptions.first {
|
|
56
84
|
let formatDescription = firstFormatDescription as! CMFormatDescription
|
|
57
85
|
let codecType = CMFormatDescriptionGetMediaSubType(formatDescription)
|
|
58
86
|
codec = fourCharCodeToString(fourCharCode: codecType)
|
|
59
87
|
}
|
|
60
|
-
|
|
88
|
+
|
|
61
89
|
// Orientation
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
orientation = (transform.b == 1.0) ? "Portrait" : "PortraitUpsideDown"
|
|
65
|
-
} else {
|
|
66
|
-
orientation = (transform.a == 1.0) ? "LandscapeRight" : "LandscapeLeft"
|
|
67
|
-
}
|
|
68
|
-
|
|
90
|
+
orientation = getOrientation(from: videoTrack)
|
|
91
|
+
|
|
69
92
|
// HDR
|
|
70
93
|
if #available(iOS 14.0, *) {
|
|
71
94
|
isHDR = videoTrack.hasMediaCharacteristic(.containsHDRVideo)
|
|
72
95
|
}
|
|
73
96
|
}
|
|
74
|
-
|
|
97
|
+
|
|
75
98
|
// Audio track information
|
|
76
99
|
if let audioTrack = asset.tracks(withMediaType: .audio).first {
|
|
77
100
|
audioSampleRate = Int(audioTrack.naturalTimeScale)
|
|
78
|
-
|
|
101
|
+
|
|
79
102
|
// Extracting audio channels from the format descriptions
|
|
80
103
|
if let formatDescriptions = audioTrack.formatDescriptions as? [CMAudioFormatDescription],
|
|
81
104
|
let firstFormatDescription = formatDescriptions.first {
|
|
82
105
|
let audioStreamBasicDescription = CMAudioFormatDescriptionGetStreamBasicDescription(firstFormatDescription)?.pointee
|
|
83
106
|
audioChannels = Int(audioStreamBasicDescription?.mChannelsPerFrame ?? 0)
|
|
84
|
-
|
|
107
|
+
|
|
85
108
|
// Extract audio codec
|
|
86
109
|
let codecType = CMFormatDescriptionGetMediaSubType(firstFormatDescription)
|
|
87
110
|
audioCodec = fourCharCodeToString(fourCharCode: codecType)
|
|
88
111
|
}
|
|
89
112
|
}
|
|
90
|
-
|
|
113
|
+
|
|
91
114
|
// Extract GPS metadata
|
|
92
115
|
if let gpsData = extractGPSData(from: asset.metadata) {
|
|
93
116
|
location = gpsData
|
|
@@ -104,6 +127,9 @@ public class ExpoVideoMetadataModule: Module {
|
|
|
104
127
|
"height": height,
|
|
105
128
|
"codec": codec,
|
|
106
129
|
"orientation": orientation,
|
|
130
|
+
"naturalOrientation": height > width ? "Portrait" : "Landscape",
|
|
131
|
+
"aspectRatio": Double(width) / Double(height),
|
|
132
|
+
"is16_9": abs((Double(width) / Double(height)) - 16.0/9.0) < 0.01,
|
|
107
133
|
"audioSampleRate": audioSampleRate,
|
|
108
134
|
"audioChannels": audioChannels,
|
|
109
135
|
"audioCodec": audioCodec,
|
|
@@ -114,12 +140,12 @@ public class ExpoVideoMetadataModule: Module {
|
|
|
114
140
|
|
|
115
141
|
private func extractGPSData(from metadata: [AVMetadataItem]) -> [String: Double]? {
|
|
116
142
|
let locationKey = "com.apple.quicktime.location.ISO6709"
|
|
117
|
-
|
|
143
|
+
|
|
118
144
|
if let locationItem = metadata.first(where: { ($0.key as? String) == locationKey }),
|
|
119
145
|
let locationString = locationItem.stringValue {
|
|
120
146
|
return parseISO6709(locationString)
|
|
121
147
|
}
|
|
122
|
-
|
|
148
|
+
|
|
123
149
|
return nil
|
|
124
150
|
}
|
|
125
151
|
|
|
@@ -128,20 +154,20 @@ private func parseISO6709(_ string: String) -> [String: Double]? {
|
|
|
128
154
|
// Where DD.DDDD is latitude, DDD.DDDD is longitude, and AAA.AAA is altitude (optional)
|
|
129
155
|
let components = string.trimmingCharacters(in: CharacterSet(charactersIn: "/")).components(separatedBy: "+")
|
|
130
156
|
guard components.count >= 3 else { return nil }
|
|
131
|
-
|
|
157
|
+
|
|
132
158
|
let latitude = Double(components[1]) ?? 0
|
|
133
159
|
let longitude = Double(components[2]) ?? 0
|
|
134
160
|
let altitude = components.count > 3 ? Double(components[3]) : nil
|
|
135
|
-
|
|
161
|
+
|
|
136
162
|
var result: [String: Double] = [
|
|
137
163
|
"latitude": latitude,
|
|
138
164
|
"longitude": longitude
|
|
139
165
|
]
|
|
140
|
-
|
|
166
|
+
|
|
141
167
|
if let altitude = altitude {
|
|
142
168
|
result["altitude"] = altitude
|
|
143
169
|
}
|
|
144
|
-
|
|
170
|
+
|
|
145
171
|
return result
|
|
146
172
|
}
|
|
147
173
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-video-metadata",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Module to retrieve metadata from a video with Web, Expo and React Native",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -33,15 +33,14 @@
|
|
|
33
33
|
"author": "Hirbod Mirjavadi (https://github.com/hirbod)",
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"homepage": "https://github.com/hirbod/expo-video-metadata#readme",
|
|
36
|
-
"dependencies": {},
|
|
37
36
|
"devDependencies": {
|
|
38
|
-
"
|
|
39
|
-
"expo-module-scripts": "^3.5.2"
|
|
40
|
-
"expo-modules-core": "^1.12.18"
|
|
37
|
+
"expo": "^52.0.23",
|
|
38
|
+
"expo-module-scripts": "^3.5.2"
|
|
41
39
|
},
|
|
42
40
|
"peerDependencies": {
|
|
43
41
|
"expo": "*",
|
|
44
42
|
"react": "*",
|
|
45
43
|
"react-native": "*"
|
|
46
|
-
}
|
|
47
|
-
|
|
44
|
+
},
|
|
45
|
+
"packageManager": "yarn@4.5.3"
|
|
46
|
+
}
|
|
@@ -43,6 +43,12 @@ export type VideoInfoResult = {
|
|
|
43
43
|
/**
|
|
44
44
|
* Video orientation.
|
|
45
45
|
* Supported on all platforms, but on Web it may return an empty string.
|
|
46
|
+
* Orientation takes into account both the natural dimensions AND any rotation/transform applied to the video:
|
|
47
|
+
* - Portrait: The video is in portrait mode.
|
|
48
|
+
* - PortraitUpsideDown: The video is in portrait mode, but upside down.
|
|
49
|
+
* - Landscape: The video is in landscape mode.
|
|
50
|
+
* - LandscapeRight: The video is in landscape mode, but rotated 90 degrees clockwise.
|
|
51
|
+
* - LandscapeLeft: The video is in landscape mode, but rotated 90 degrees counter-clockwise.
|
|
46
52
|
*/
|
|
47
53
|
orientation:
|
|
48
54
|
| "Portrait"
|
|
@@ -50,6 +56,21 @@ export type VideoInfoResult = {
|
|
|
50
56
|
| "Landscape"
|
|
51
57
|
| "LandscapeRight"
|
|
52
58
|
| "LandscapeLeft";
|
|
59
|
+
/**
|
|
60
|
+
* Natural orientation of the video.
|
|
61
|
+
* This is the orientation of the video as it was recorded, without any rotation/transform applied.
|
|
62
|
+
*/
|
|
63
|
+
naturalOrientation:
|
|
64
|
+
| "Portrait"
|
|
65
|
+
| "Landscape";
|
|
66
|
+
/**
|
|
67
|
+
* Aspect ratio of the video.
|
|
68
|
+
*/
|
|
69
|
+
aspectRatio: number;
|
|
70
|
+
/**
|
|
71
|
+
* Tells if the video is 16:9.
|
|
72
|
+
*/
|
|
73
|
+
is16_9: boolean;
|
|
53
74
|
/**
|
|
54
75
|
* Audio sample rate of the video in samples per second.
|
|
55
76
|
*/
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { requireNativeModule } from "expo
|
|
1
|
+
import { requireNativeModule } from "expo";
|
|
2
2
|
export default requireNativeModule("ExpoVideoMetadata");
|
|
@@ -32,6 +32,49 @@ interface HTMLVideoElementWithTracks extends HTMLVideoElement {
|
|
|
32
32
|
export default {
|
|
33
33
|
name: "ExpoVideoMetadata",
|
|
34
34
|
|
|
35
|
+
getVideoOrientation(video: HTMLVideoElementWithTracks) {
|
|
36
|
+
// Get the video element's transform style
|
|
37
|
+
const transform = window.getComputedStyle(video).transform;
|
|
38
|
+
const matrix = new DOMMatrix(transform);
|
|
39
|
+
|
|
40
|
+
// Calculate rotation angle from transform matrix
|
|
41
|
+
const rotation = Math.round(Math.atan2(matrix.b, matrix.a) * (180 / Math.PI));
|
|
42
|
+
|
|
43
|
+
// Get natural dimensions
|
|
44
|
+
const { videoWidth: width, videoHeight: height } = video;
|
|
45
|
+
const isNaturallyPortrait = height > width;
|
|
46
|
+
|
|
47
|
+
// First check if there's rotation applied via CSS transform
|
|
48
|
+
if (rotation !== 0) {
|
|
49
|
+
switch ((rotation + 360) % 360) {
|
|
50
|
+
case 90:
|
|
51
|
+
return "Portrait";
|
|
52
|
+
case 270:
|
|
53
|
+
return "PortraitUpsideDown";
|
|
54
|
+
case 0:
|
|
55
|
+
return isNaturallyPortrait ? "Portrait" : "LandscapeRight";
|
|
56
|
+
case 180:
|
|
57
|
+
return isNaturallyPortrait ? "PortraitUpsideDown" : "LandscapeLeft";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// If no rotation, use natural dimensions
|
|
62
|
+
// Check for exact 16:9 ratio to handle the special case
|
|
63
|
+
const aspectRatio = width / height;
|
|
64
|
+
const is16_9 = Math.abs(aspectRatio - 16/9) < 0.01;
|
|
65
|
+
|
|
66
|
+
if (is16_9) {
|
|
67
|
+
// For 16:9 videos, try to detect orientation from video track data
|
|
68
|
+
const videoTrack = video.videoTracks?.[0];
|
|
69
|
+
if (videoTrack?.label?.toLowerCase().includes('portrait')) {
|
|
70
|
+
return "Portrait";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Default to dimensions-based orientation
|
|
75
|
+
return isNaturallyPortrait ? "Portrait" : "LandscapeRight";
|
|
76
|
+
},
|
|
77
|
+
|
|
35
78
|
getVideoFrameRate(videoElement: HTMLVideoElementWithTracks) {
|
|
36
79
|
if (!videoElement.captureStream) {
|
|
37
80
|
console.info("captureStream method not supported");
|
|
@@ -68,15 +111,12 @@ export default {
|
|
|
68
111
|
};
|
|
69
112
|
} catch (decodeError) {
|
|
70
113
|
console.info("Error decoding audio data (no audio?):", decodeError);
|
|
71
|
-
// If decoding fails, assume no audio
|
|
72
114
|
return { sampleRate: 0, numberOfChannels: 0 };
|
|
73
115
|
} finally {
|
|
74
116
|
await audioContext.close();
|
|
75
117
|
}
|
|
76
118
|
} catch (error) {
|
|
77
119
|
console.info("Error decoding audio data (CORS?):", error);
|
|
78
|
-
|
|
79
|
-
// If any error occurs during the process, return default values silently
|
|
80
120
|
return { sampleRate: 0, numberOfChannels: 0 };
|
|
81
121
|
}
|
|
82
122
|
},
|
|
@@ -136,8 +176,7 @@ export default {
|
|
|
136
176
|
await new Promise<void>((resolve, reject) => {
|
|
137
177
|
// Can't use `loadedmetadata` event because it does not contain videoTracks, audioTracks and other metadata
|
|
138
178
|
video.onloadeddata = () => resolve();
|
|
139
|
-
video.onerror = () =>
|
|
140
|
-
reject(new Error("Failed to load video metadata"));
|
|
179
|
+
video.onerror = () => reject(new Error("Failed to load video metadata"));
|
|
141
180
|
video.src = videoUrl;
|
|
142
181
|
video.load();
|
|
143
182
|
video.pause();
|
|
@@ -161,9 +200,14 @@ export default {
|
|
|
161
200
|
fileSize && duration ? Math.floor(fileSize / duration) : 0;
|
|
162
201
|
|
|
163
202
|
const { numberOfChannels: audioChannels, sampleRate: audioSampleRate } =
|
|
164
|
-
await this.getAudioBuffer(videoUrl);
|
|
203
|
+
await this.getAudioBuffer(videoUrl, options);
|
|
165
204
|
|
|
166
205
|
const fps = this.getVideoFrameRate(video);
|
|
206
|
+
const orientation = this.getVideoOrientation(video);
|
|
207
|
+
|
|
208
|
+
// Calculate additional metadata
|
|
209
|
+
const aspectRatio = width / height;
|
|
210
|
+
const is16_9 = Math.abs(aspectRatio - 16/9) < 0.01;
|
|
167
211
|
|
|
168
212
|
return {
|
|
169
213
|
duration,
|
|
@@ -178,11 +222,14 @@ export default {
|
|
|
178
222
|
codec: videoTrack?.label ?? "",
|
|
179
223
|
audioChannels,
|
|
180
224
|
fps,
|
|
181
|
-
orientation
|
|
225
|
+
orientation,
|
|
226
|
+
aspectRatio,
|
|
227
|
+
is16_9,
|
|
228
|
+
naturalOrientation: width >= height ? "Landscape" : "Portrait",
|
|
182
229
|
location: null, // not supported on web
|
|
183
230
|
};
|
|
184
231
|
} finally {
|
|
185
232
|
resetVideo();
|
|
186
233
|
}
|
|
187
234
|
},
|
|
188
|
-
};
|
|
235
|
+
};
|