expo-video-metadata 1.2.1 → 1.3.1

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/README.md CHANGED
@@ -42,3 +42,14 @@ const result = await getVideoInfoAsync(sourceFilename: string | File | Blob, opt
42
42
  ```
43
43
 
44
44
  See [VideoInfoResult](https://github.com/hirbod/expo-video-metadata/blob/main/src/ExpoVideoMetadata.types.ts#L1) type for more information.
45
+
46
+ ## Hints
47
+
48
+ If you're using libraries like expo-image-picker, make sure to use [preferredAssetRepresentationMode](https://docs.expo.dev/versions/latest/sdk/imagepicker/#imagepickeroptions) option like this:
49
+
50
+ ```ts
51
+ preferredAssetRepresentationMode: ImagePicker
52
+ .UIImagePickerPreferredAssetRepresentationMode.Current;
53
+ ```
54
+
55
+ when picking a video. This will avoid the need to copy or transcode the video file and thus be a lot faster on iOS. If you use a different library, make sure to use the equivalent option. Location data is not supported with expo-image-picker, unless you set `legacy` to `true`.
@@ -76,6 +76,9 @@ class ExpoVideoMetadataModule : Module() {
76
76
  isHDR = colorTransfer == MediaFormat.COLOR_TRANSFER_ST2084 || colorTransfer == MediaFormat.COLOR_TRANSFER_HLG
77
77
  }
78
78
 
79
+ // Extract GPS location
80
+ val location = extractGPSLocation(retriever)
81
+
79
82
  // release
80
83
  retriever.release()
81
84
 
@@ -128,7 +131,8 @@ class ExpoVideoMetadataModule : Module() {
128
131
  "audioSampleRate" to audioSampleRate,
129
132
  "audioCodec" to audioCodec,
130
133
  "codec" to videoCodec,
131
- "fps" to frameRate
134
+ "fps" to frameRate,
135
+ "location" to location
132
136
  )
133
137
  )
134
138
  } catch (e: Exception) {
@@ -148,6 +152,44 @@ class ExpoVideoMetadataModule : Module() {
148
152
  }
149
153
  }
150
154
 
155
+ private fun extractGPSLocation(retriever: MediaMetadataRetriever): Map<String, Double>? {
156
+ val locationString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
157
+ Log.d(TAG, "Raw location string: $locationString")
158
+
159
+ if (locationString != null) {
160
+ // Remove the leading "+" and trailing "/"
161
+ val cleanedString = locationString.trim('+', '/')
162
+
163
+ // Split the string into components
164
+ val parts = cleanedString.split("+")
165
+
166
+ if (parts.size >= 2) {
167
+ val latitude = parts[0].toDoubleOrNull()
168
+ val longitude = parts[1].toDoubleOrNull()
169
+ val altitude = if (parts.size >= 3) parts[2].toDoubleOrNull() else null
170
+
171
+ if (latitude != null && longitude != null) {
172
+ Log.d(TAG, "Parsed location: lat=$latitude, lon=$longitude, alt=$altitude")
173
+ return buildMap {
174
+ put("latitude", latitude)
175
+ put("longitude", longitude)
176
+ if (altitude != null) {
177
+ put("altitude", altitude)
178
+ }
179
+ }
180
+ } else {
181
+ Log.w(TAG, "Failed to parse GPS coordinates from location string")
182
+ }
183
+ } else {
184
+ Log.w(TAG, "Invalid GPS location format in metadata")
185
+ }
186
+ } else {
187
+ Log.i(TAG, "GPS location not found in video metadata")
188
+ }
189
+
190
+ return null
191
+ }
192
+
151
193
  private fun isAllowedToRead(url: String): Boolean {
152
194
  val permissionModuleInterface = appContext.filePermission
153
195
  ?: throw FilePermissionsModuleNotFound()
@@ -57,6 +57,15 @@ export type VideoInfoResult = {
57
57
  * Audio codec of the video.
58
58
  */
59
59
  audioCodec: string;
60
+ /**
61
+ * Location where the video was recorded.
62
+ * Supported on iOS and Android (if the video contains location metadata)
63
+ */
64
+ location: {
65
+ latitude: number;
66
+ longitude: number;
67
+ altitude?: number;
68
+ } | null;
60
69
  };
61
70
  export type VideoSource = string | File | Blob;
62
71
  export type VideoInfoOptions = {
@@ -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;CACpB,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;;;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 +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\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 */\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 +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;oCAyB5B,MAAM,GAAG,MAAM;qBAKxB,MAAM,YAAW,WAAW,GAAQ,QAAQ,MAAM,CAAC;yBAgBhE,WAAW,YACV,gBAAgB,GACxB,QAAQ,eAAe,CAAC;;AAvE7B,wBAuJE"}
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"}
@@ -14,20 +14,31 @@ export default {
14
14
  return videoTrack.getSettings().frameRate ?? 0;
15
15
  },
16
16
  async getAudioBuffer(audioUrl, options = {}) {
17
- const audioContext = new (window.AudioContext ||
18
- window.webkitAudioContext)();
19
17
  try {
20
18
  const response = await fetch(audioUrl, options);
21
19
  const arrayBuffer = await response.arrayBuffer();
22
- return new Promise((resolve, reject) => {
23
- audioContext.decodeAudioData(arrayBuffer, ({ sampleRate, numberOfChannels }) => resolve({ sampleRate, numberOfChannels }), reject);
24
- });
20
+ const audioContext = new (window.AudioContext ||
21
+ window.webkitAudioContext)();
22
+ try {
23
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
24
+ return {
25
+ sampleRate: audioBuffer.sampleRate,
26
+ numberOfChannels: audioBuffer.numberOfChannels,
27
+ };
28
+ }
29
+ catch (decodeError) {
30
+ console.info("Error decoding audio data (no audio?):", decodeError);
31
+ // If decoding fails, assume no audio
32
+ return { sampleRate: 0, numberOfChannels: 0 };
33
+ }
34
+ finally {
35
+ await audioContext.close();
36
+ }
25
37
  }
26
38
  catch (error) {
27
- throw new Error(`Error fetching or decoding audio file: ${error.message}`);
28
- }
29
- finally {
30
- await audioContext.close();
39
+ console.info("Error decoding audio data (CORS?):", error);
40
+ // If any error occurs during the process, return default values silently
41
+ return { sampleRate: 0, numberOfChannels: 0 };
31
42
  }
32
43
  },
33
44
  getBase64FileSize(base64String) {
@@ -44,7 +55,7 @@ export default {
44
55
  return contentLength ? parseInt(contentLength, 10) : 0;
45
56
  }
46
57
  catch (error) {
47
- console.error("Error fetching file size for URL:", url, error);
58
+ console.info("Error fetching file size for URL (CORS?):", url, error);
48
59
  return 0;
49
60
  }
50
61
  },
@@ -102,12 +113,13 @@ export default {
102
113
  fileSize,
103
114
  hasAudio,
104
115
  audioSampleRate,
105
- isHDR: null,
116
+ isHDR: null, // not supported on web
106
117
  audioCodec: audioTrack?.label ?? "",
107
118
  codec: videoTrack?.label ?? "",
108
119
  audioChannels,
109
120
  fps,
110
121
  orientation: width >= height ? "Landscape" : "Portrait",
122
+ location: null, // not supported on web
111
123
  };
112
124
  }
113
125
  finally {
@@ -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 const audioContext = new (window.AudioContext ||\n (window as any).webkitAudioContext)();\n\n try {\n const response = await fetch(audioUrl, options);\n const arrayBuffer = await response.arrayBuffer();\n\n return new Promise((resolve, reject) => {\n audioContext.decodeAudioData(\n arrayBuffer,\n ({ sampleRate, numberOfChannels }) =>\n resolve({ sampleRate, numberOfChannels }),\n reject\n );\n });\n } catch (error) {\n throw new Error(\n `Error fetching or decoding audio file: ${error.message}`\n );\n } finally {\n await audioContext.close();\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.error(\"Error fetching file size for URL:\", 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,\n audioCodec: audioTrack?.label ?? \"\",\n codec: videoTrack?.label ?? \"\",\n audioChannels,\n fps,\n orientation: width >= height ? \"Landscape\" : \"Portrait\",\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 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"]}
@@ -4,27 +4,27 @@ import AVFoundation
4
4
  public class ExpoVideoMetadataModule: Module {
5
5
  public func definition() -> ModuleDefinition {
6
6
  Name("ExpoVideoMetadata")
7
-
8
- AsyncFunction("getVideoInfo", getVideoInfo).runOnQueue(.main)
7
+
8
+ AsyncFunction("getVideoInfo", getVideoInfo)
9
9
  }
10
-
10
+
11
11
  internal func getVideoInfo(sourceFilename: URL, options: ExpoVideoMetadataOptions) throws -> [String: Any] {
12
12
  if sourceFilename.isFileURL {
13
13
  guard FileSystemUtilities.permissions(appContext, for: sourceFilename).contains(.read) else {
14
14
  throw FileSystemReadPermissionException(sourceFilename.absoluteString)
15
15
  }
16
16
  }
17
-
17
+
18
18
  let asset = AVURLAsset.init(url: sourceFilename, options: ["AVURLAssetHTTPHeaderFieldsKey": options.headers])
19
19
  let duration = CMTimeGetSeconds(asset.duration)
20
20
  let hasAudio = asset.tracks(withMediaType: .audio).count > 0
21
-
21
+
22
22
  var fileSize: Int64 = 0
23
23
  if let fileAttributes = try? FileManager.default.attributesOfItem(atPath: sourceFilename.path),
24
24
  let size = fileAttributes[.size] as? NSNumber {
25
25
  fileSize = size.int64Value
26
26
  }
27
-
27
+
28
28
  // Initialize default values
29
29
  var bitrate: Float = 0.0
30
30
  var width: Int = 0
@@ -36,27 +36,28 @@ public class ExpoVideoMetadataModule: Module {
36
36
  var audioSampleRate: Int = 0
37
37
  var audioChannels: Int = 0
38
38
  var audioCodec: String = ""
39
-
39
+ var location: [String: Double]? = nil
40
+
40
41
  // If there are video tracks, extract more information
41
42
  if let videoTrack = asset.tracks(withMediaType: .video).first {
42
43
  // Bitrate
43
44
  bitrate = videoTrack.estimatedDataRate
44
-
45
+
45
46
  // Width and Height
46
47
  let size = videoTrack.naturalSize
47
48
  width = Int(size.width)
48
49
  height = Int(size.height)
49
-
50
+
50
51
  // Frame Rate
51
52
  frameRate = videoTrack.nominalFrameRate
52
-
53
+
53
54
  // Codec
54
55
  if let firstFormatDescription = videoTrack.formatDescriptions.first {
55
56
  let formatDescription = firstFormatDescription as! CMFormatDescription
56
57
  let codecType = CMFormatDescriptionGetMediaSubType(formatDescription)
57
58
  codec = fourCharCodeToString(fourCharCode: codecType)
58
59
  }
59
-
60
+
60
61
  // Orientation
61
62
  let transform = videoTrack.preferredTransform
62
63
  if transform.a == 0 && transform.d == 0 {
@@ -64,29 +65,34 @@ public class ExpoVideoMetadataModule: Module {
64
65
  } else {
65
66
  orientation = (transform.a == 1.0) ? "LandscapeRight" : "LandscapeLeft"
66
67
  }
67
-
68
+
68
69
  // HDR
69
70
  if #available(iOS 14.0, *) {
70
71
  isHDR = videoTrack.hasMediaCharacteristic(.containsHDRVideo)
71
72
  }
72
73
  }
73
-
74
+
74
75
  // Audio track information
75
76
  if let audioTrack = asset.tracks(withMediaType: .audio).first {
76
77
  audioSampleRate = Int(audioTrack.naturalTimeScale)
77
-
78
+
78
79
  // Extracting audio channels from the format descriptions
79
80
  if let formatDescriptions = audioTrack.formatDescriptions as? [CMAudioFormatDescription],
80
81
  let firstFormatDescription = formatDescriptions.first {
81
82
  let audioStreamBasicDescription = CMAudioFormatDescriptionGetStreamBasicDescription(firstFormatDescription)?.pointee
82
83
  audioChannels = Int(audioStreamBasicDescription?.mChannelsPerFrame ?? 0)
83
-
84
+
84
85
  // Extract audio codec
85
86
  let codecType = CMFormatDescriptionGetMediaSubType(firstFormatDescription)
86
87
  audioCodec = fourCharCodeToString(fourCharCode: codecType)
87
88
  }
88
89
  }
89
-
90
+
91
+ // Extract GPS metadata
92
+ if let gpsData = extractGPSData(from: asset.metadata) {
93
+ location = gpsData
94
+ }
95
+
90
96
  return [
91
97
  "duration": duration,
92
98
  "hasAudio": hasAudio,
@@ -100,18 +106,54 @@ public class ExpoVideoMetadataModule: Module {
100
106
  "orientation": orientation,
101
107
  "audioSampleRate": audioSampleRate,
102
108
  "audioChannels": audioChannels,
103
- "audioCodec": audioCodec
109
+ "audioCodec": audioCodec,
110
+ "location": location as Any
104
111
  ]
105
112
  }
113
+ }
106
114
 
107
- // Helper function to convert FourCC code to String
108
- private func fourCharCodeToString(fourCharCode: FourCharCode) -> String {
109
- let characters = [
110
- Character(UnicodeScalar((fourCharCode >> 24) & 0xFF)!),
111
- Character(UnicodeScalar((fourCharCode >> 16) & 0xFF)!),
112
- Character(UnicodeScalar((fourCharCode >> 8) & 0xFF)!),
113
- Character(UnicodeScalar(fourCharCode & 0xFF)!)
114
- ]
115
- return String(characters)
115
+ private func extractGPSData(from metadata: [AVMetadataItem]) -> [String: Double]? {
116
+ let locationKey = "com.apple.quicktime.location.ISO6709"
117
+
118
+ if let locationItem = metadata.first(where: { ($0.key as? String) == locationKey }),
119
+ let locationString = locationItem.stringValue {
120
+ return parseISO6709(locationString)
116
121
  }
122
+
123
+ return nil
117
124
  }
125
+
126
+ private func parseISO6709(_ string: String) -> [String: Double]? {
127
+ // Format: +DD.DDDD+DDD.DDDD+AAA.AAA/
128
+ // Where DD.DDDD is latitude, DDD.DDDD is longitude, and AAA.AAA is altitude (optional)
129
+ let components = string.trimmingCharacters(in: CharacterSet(charactersIn: "/")).components(separatedBy: "+")
130
+ guard components.count >= 3 else { return nil }
131
+
132
+ let latitude = Double(components[1]) ?? 0
133
+ let longitude = Double(components[2]) ?? 0
134
+ let altitude = components.count > 3 ? Double(components[3]) : nil
135
+
136
+ var result: [String: Double] = [
137
+ "latitude": latitude,
138
+ "longitude": longitude
139
+ ]
140
+
141
+ if let altitude = altitude {
142
+ result["altitude"] = altitude
143
+ }
144
+
145
+ return result
146
+ }
147
+
148
+ // Helper function to convert FourCC code to String
149
+ private func fourCharCodeToString(fourCharCode: FourCharCode) -> String {
150
+ let characters = [
151
+ Character(UnicodeScalar((fourCharCode >> 24) & 0xFF)!),
152
+ Character(UnicodeScalar((fourCharCode >> 16) & 0xFF)!),
153
+ Character(UnicodeScalar((fourCharCode >> 8) & 0xFF)!),
154
+ Character(UnicodeScalar(fourCharCode & 0xFF)!)
155
+ ]
156
+ // Remove any trailing whitespaces, since FourCC codes are 4 characters long and padded with spaces ("aac " for example)
157
+ return String(characters).trimmingCharacters(in: .whitespaces)
158
+ }
159
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-video-metadata",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
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",
@@ -62,6 +62,15 @@ export type VideoInfoResult = {
62
62
  * Audio codec of the video.
63
63
  */
64
64
  audioCodec: string;
65
+ /**
66
+ * Location where the video was recorded.
67
+ * Supported on iOS and Android (if the video contains location metadata)
68
+ */
69
+ location: {
70
+ latitude: number;
71
+ longitude: number;
72
+ altitude?: number;
73
+ } | null;
65
74
  };
66
75
 
67
76
  export type VideoSource = string | File | Blob;
@@ -53,27 +53,31 @@ export default {
53
53
  audioUrl: string,
54
54
  options: RequestInit = {}
55
55
  ): Promise<{ sampleRate: number; numberOfChannels: number }> {
56
- const audioContext = new (window.AudioContext ||
57
- (window as any).webkitAudioContext)();
58
-
59
56
  try {
60
57
  const response = await fetch(audioUrl, options);
61
58
  const arrayBuffer = await response.arrayBuffer();
62
59
 
63
- return new Promise((resolve, reject) => {
64
- audioContext.decodeAudioData(
65
- arrayBuffer,
66
- ({ sampleRate, numberOfChannels }) =>
67
- resolve({ sampleRate, numberOfChannels }),
68
- reject
69
- );
70
- });
60
+ const audioContext = new (window.AudioContext ||
61
+ (window as any).webkitAudioContext)();
62
+
63
+ try {
64
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
65
+ return {
66
+ sampleRate: audioBuffer.sampleRate,
67
+ numberOfChannels: audioBuffer.numberOfChannels,
68
+ };
69
+ } catch (decodeError) {
70
+ console.info("Error decoding audio data (no audio?):", decodeError);
71
+ // If decoding fails, assume no audio
72
+ return { sampleRate: 0, numberOfChannels: 0 };
73
+ } finally {
74
+ await audioContext.close();
75
+ }
71
76
  } catch (error) {
72
- throw new Error(
73
- `Error fetching or decoding audio file: ${error.message}`
74
- );
75
- } finally {
76
- await audioContext.close();
77
+ console.info("Error decoding audio data (CORS?):", error);
78
+
79
+ // If any error occurs during the process, return default values silently
80
+ return { sampleRate: 0, numberOfChannels: 0 };
77
81
  }
78
82
  },
79
83
 
@@ -92,7 +96,7 @@ export default {
92
96
  const contentLength = response.headers.get("Content-Length");
93
97
  return contentLength ? parseInt(contentLength, 10) : 0;
94
98
  } catch (error) {
95
- console.error("Error fetching file size for URL:", url, error);
99
+ console.info("Error fetching file size for URL (CORS?):", url, error);
96
100
  return 0;
97
101
  }
98
102
  },
@@ -169,12 +173,13 @@ export default {
169
173
  fileSize,
170
174
  hasAudio,
171
175
  audioSampleRate,
172
- isHDR: null,
176
+ isHDR: null, // not supported on web
173
177
  audioCodec: audioTrack?.label ?? "",
174
178
  codec: videoTrack?.label ?? "",
175
179
  audioChannels,
176
180
  fps,
177
181
  orientation: width >= height ? "Landscape" : "Portrait",
182
+ location: null, // not supported on web
178
183
  };
179
184
  } finally {
180
185
  resetVideo();