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 ADDED
@@ -0,0 +1,3 @@
1
+ nodeLinker: node-modules
2
+
3
+ yarnPath: .yarn/releases/yarn-4.5.3.cjs
@@ -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 getOrientation(rotation),
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
- return when (rotation) {
201
- 0 -> "LandscapeRight"
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 -> "LandscapeRight" // Default or unknown rotation
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" // AAC Audio
215
- mimeType.contains("ac3") -> "ac3" // AC3 Audio
216
- mimeType.contains("opus") -> "opus" // Opus Audio
217
- mimeType.contains("vorbis") -> "vorbis" // Vorbis Audio
218
- mimeType.contains("flac") -> "flac" // FLAC Audio
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" // H.264/AVC Video
226
- mimeType.contains("hev") || mimeType.contains("h265") -> "hev1" // H.265/HEVC Video
227
- mimeType.contains("vp9") -> "vp9" // VP9 Video
228
- mimeType.contains("vp8") -> "vp8" // VP8 Video
229
- mimeType.contains("mp4v-es") -> "mp4v" // MPEG-4 Video
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;;;OAGG;IACH,WAAW,EACP,UAAU,GACV,oBAAoB,GACpB,WAAW,GACX,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;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
+ {"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,3 +1,3 @@
1
- import { requireNativeModule } from "expo-modules-core";
1
+ import { requireNativeModule } from "expo";
2
2
  export default requireNativeModule("ExpoVideoMetadata");
3
3
  //# sourceMappingURL=ExpoVideoMetadataModule.js.map
@@ -1 +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"]}
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;;;oCAKiC,0BAA0B;6BAkB9C,MAAM,YACP,WAAW,GACnB,QAAQ;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,CAAC;oCA6B5B,MAAM,GAAG,MAAM;qBAKxB,MAAM,YAAW,WAAW,GAAQ,QAAQ,MAAM,CAAC;yBAgBhE,WAAW,YACV,gBAAgB,GACxB,QAAQ,eAAe,CAAC;;AA3E7B,wBA4JE"}
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: width >= height ? "Landscape" : "Portrait",
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
- let transform = videoTrack.preferredTransform
63
- if transform.a == 0 && transform.d == 0 {
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.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
- "@types/react": "^18.2.45",
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-modules-core";
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: width >= height ? "Landscape" : "Portrait",
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
+ };