expo-video-metadata 1.2.0 → 1.3.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/README.md CHANGED
@@ -32,13 +32,24 @@ import { getVideoInfoAsync } from 'expo-video-metadata';
32
32
  /**
33
33
  * Retrieves video metadata.
34
34
  *
35
- * @param sourceFilename An URI of the video, local or remote.
35
+ * @param sourceFilename An URI of the video, local or remote. On web, it can be a File or Blob object, too. base64 URIs are supported but not recommended, as they can be very large and cause performance issues.
36
36
  * @param options Pass `headers` object in case `sourceFilename` is a remote URI, e.g { headers: "Authorization": "Bearer some-token" } etc.
37
37
  *
38
38
  * @return Returns a promise which fulfils with [`VideoInfoResult`](#Videoinforesult).
39
39
  */
40
40
 
41
- const result = await getVideoInfoAsync(sourceFilename: string, options: VideoInfoOptions = {}): Promise<VideoInfoResult>
41
+ const result = await getVideoInfoAsync(sourceFilename: string | File | Blob, options: VideoInfoOptions = {}): Promise<VideoInfoResult>
42
42
  ```
43
43
 
44
- See [VideoInfoResult](https://github.com/hirbod/expo-video-metadata/blob/b9239224eed46f455b2fb9f1b29e69ac49da6683/src/ExpoVideoMetadata.types.ts#L1) type for more information.
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,7 +57,17 @@ 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
  };
70
+ export type VideoSource = string | File | Blob;
61
71
  export type VideoInfoOptions = {
62
72
  /**
63
73
  * In case `sourceFilename` is a remote URI, `headers` object is passed in a network request.
@@ -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,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 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,4 +1,4 @@
1
- import type { VideoInfoOptions, VideoInfoResult } from "./ExpoVideoMetadata.types";
1
+ import type { VideoInfoOptions, VideoInfoResult, VideoSource } from "./ExpoVideoMetadata.types";
2
2
  interface Track {
3
3
  id: string;
4
4
  kind: string;
@@ -28,7 +28,7 @@ declare const _default: {
28
28
  }>;
29
29
  getBase64FileSize(base64String: string): number;
30
30
  getFileSize(url: string, options?: RequestInit): Promise<number>;
31
- getVideoInfo(sourceFilename: string | File | Blob, options?: VideoInfoOptions): Promise<VideoInfoResult>;
31
+ getVideoInfo(source: VideoSource, options?: VideoInfoOptions): Promise<VideoInfoResult>;
32
32
  };
33
33
  export default _default;
34
34
  //# sourceMappingURL=ExpoVideoMetadataModule.web.d.ts.map
@@ -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,EAChB,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;iCAgBxD,MAAM,GAAG,IAAI,GAAG,IAAI,YAC3B,gBAAgB,GACxB,QAAQ,eAAe,CAAC;;AAvE7B,wBAyJE"}
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,wBAwJE"}
@@ -48,15 +48,14 @@ export default {
48
48
  return 0;
49
49
  }
50
50
  },
51
- async getVideoInfo(sourceFilename, options = {}) {
51
+ async getVideoInfo(source, options = {}) {
52
52
  const video = document.createElement("video");
53
53
  let videoUrl = "";
54
- if (typeof sourceFilename === "string") {
55
- videoUrl = sourceFilename;
54
+ if (typeof source === "string") {
55
+ videoUrl = source;
56
56
  }
57
- else if (sourceFilename instanceof File ||
58
- sourceFilename instanceof Blob) {
59
- videoUrl = URL.createObjectURL(sourceFilename);
57
+ else if (source instanceof File || source instanceof Blob) {
58
+ videoUrl = URL.createObjectURL(source);
60
59
  }
61
60
  Object.assign(video, {
62
61
  crossOrigin: "anonymous",
@@ -69,6 +68,10 @@ export default {
69
68
  video.removeAttribute("src");
70
69
  video.load();
71
70
  video.remove();
71
+ // Revoke the object URL if it was created
72
+ if (source instanceof File || source instanceof Blob) {
73
+ URL.revokeObjectURL(videoUrl);
74
+ }
72
75
  };
73
76
  try {
74
77
  await new Promise((resolve, reject) => {
@@ -85,8 +88,8 @@ export default {
85
88
  const hasAudio = Boolean(video.audioTracks?.length) ||
86
89
  video.mozHasAudio ||
87
90
  Boolean(video.webkitAudioDecodedByteCount);
88
- const fileSize = sourceFilename instanceof File || sourceFilename instanceof Blob
89
- ? sourceFilename.size
91
+ const fileSize = source instanceof File || source instanceof Blob
92
+ ? source.size
90
93
  : await this.getFileSize(videoUrl, options);
91
94
  const bitRate = fileSize && duration ? Math.floor(fileSize / duration) : 0;
92
95
  const { numberOfChannels: audioChannels, sampleRate: audioSampleRate } = await this.getAudioBuffer(videoUrl);
@@ -99,19 +102,17 @@ export default {
99
102
  fileSize,
100
103
  hasAudio,
101
104
  audioSampleRate,
102
- isHDR: null,
105
+ isHDR: null, // not supported on web
103
106
  audioCodec: audioTrack?.label ?? "",
104
107
  codec: videoTrack?.label ?? "",
105
108
  audioChannels,
106
109
  fps,
107
110
  orientation: width >= height ? "Landscape" : "Portrait",
111
+ location: null, // not supported on web
108
112
  };
109
113
  }
110
114
  finally {
111
115
  resetVideo();
112
- if (sourceFilename instanceof File || sourceFilename instanceof Blob) {
113
- URL.revokeObjectURL(videoUrl);
114
- }
115
116
  }
116
117
  },
117
118
  };
@@ -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} 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 sourceFilename: string | File | Blob,\n options: VideoInfoOptions = {}\n ): Promise<VideoInfoResult> {\n const video = document.createElement(\"video\") as HTMLVideoElementWithTracks;\n let videoUrl = \"\";\n\n if (typeof sourceFilename === \"string\") {\n videoUrl = sourceFilename;\n } else if (\n sourceFilename instanceof File ||\n sourceFilename instanceof Blob\n ) {\n videoUrl = URL.createObjectURL(sourceFilename);\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 };\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 sourceFilename instanceof File || sourceFilename instanceof Blob\n ? sourceFilename.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 if (sourceFilename instanceof File || sourceFilename instanceof Blob) {\n URL.revokeObjectURL(videoUrl);\n }\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 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, // 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"]}
package/build/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
- import { VideoInfoOptions, VideoInfoResult } from "./ExpoVideoMetadata.types";
1
+ import { VideoInfoOptions, VideoInfoResult, VideoSource } from "./ExpoVideoMetadata.types";
2
2
  export { VideoInfoOptions, VideoInfoResult };
3
3
  /**
4
4
  * Retrieves video metadata.
5
5
  *
6
- * @param sourceFilename An URI of the video, local or remote.
6
+ * @param source An URI (string) of the video, local or remote. On web, it can be a File or Blob object, too. base64 URIs are supported but not recommended, as they can be very large and cause performance issues.
7
7
  * @param options Pass `headers` object in case `sourceFilename` is a remote URI, e.g { headers: "Authorization": "Bearer some-token" } etc.
8
8
  *
9
9
  * @return Returns a promise which fulfils with [`VideoInfoResult`](#Videoinforesult).
10
10
  */
11
- export declare function getVideoInfoAsync(sourceFilename: string | File | Blob, options?: VideoInfoOptions): Promise<VideoInfoResult>;
11
+ export declare function getVideoInfoAsync(source: VideoSource, options?: VideoInfoOptions): Promise<VideoInfoResult>;
12
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAG9E,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,CAAC;AAK7C;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,EACpC,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAE1B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,WAAW,EACZ,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,CAAC;AAK7C;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,WAAW,EACnB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAE1B"}
package/build/index.js CHANGED
@@ -4,12 +4,12 @@ import ExpoVideoMetadataModule from "./ExpoVideoMetadataModule";
4
4
  /**
5
5
  * Retrieves video metadata.
6
6
  *
7
- * @param sourceFilename An URI of the video, local or remote.
7
+ * @param source An URI (string) of the video, local or remote. On web, it can be a File or Blob object, too. base64 URIs are supported but not recommended, as they can be very large and cause performance issues.
8
8
  * @param options Pass `headers` object in case `sourceFilename` is a remote URI, e.g { headers: "Authorization": "Bearer some-token" } etc.
9
9
  *
10
10
  * @return Returns a promise which fulfils with [`VideoInfoResult`](#Videoinforesult).
11
11
  */
12
- export async function getVideoInfoAsync(sourceFilename, options = {}) {
13
- return await ExpoVideoMetadataModule.getVideoInfo(sourceFilename, options);
12
+ export async function getVideoInfoAsync(source, options = {}) {
13
+ return await ExpoVideoMetadataModule.getVideoInfo(source, options);
14
14
  }
15
15
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,uBAAuB,MAAM,2BAA2B,CAAC;AAIhE,oFAAoF;AACpF,kDAAkD;AAElD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,cAAoC,EACpC,UAA4B,EAAE;IAE9B,OAAO,MAAM,uBAAuB,CAAC,YAAY,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;AAC7E,CAAC","sourcesContent":["import { VideoInfoOptions, VideoInfoResult } from \"./ExpoVideoMetadata.types\";\nimport ExpoVideoMetadataModule from \"./ExpoVideoMetadataModule\";\n\nexport { VideoInfoOptions, VideoInfoResult };\n\n// Import the native module. On web, it will be resolved to ExpoVideoMetadata.web.ts\n// and on native platforms to ExpoVideoMetadata.ts\n\n/**\n * Retrieves video metadata.\n *\n * @param sourceFilename An URI of the video, local or remote.\n * @param options Pass `headers` object in case `sourceFilename` is a remote URI, e.g { headers: \"Authorization\": \"Bearer some-token\" } etc.\n *\n * @return Returns a promise which fulfils with [`VideoInfoResult`](#Videoinforesult).\n */\nexport async function getVideoInfoAsync(\n sourceFilename: string | File | Blob,\n options: VideoInfoOptions = {}\n): Promise<VideoInfoResult> {\n return await ExpoVideoMetadataModule.getVideoInfo(sourceFilename, options);\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,uBAAuB,MAAM,2BAA2B,CAAC;AAIhE,oFAAoF;AACpF,kDAAkD;AAElD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,MAAmB,EACnB,UAA4B,EAAE;IAE9B,OAAO,MAAM,uBAAuB,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AACrE,CAAC","sourcesContent":["import {\n VideoInfoOptions,\n VideoInfoResult,\n VideoSource,\n} from \"./ExpoVideoMetadata.types\";\nimport ExpoVideoMetadataModule from \"./ExpoVideoMetadataModule\";\n\nexport { VideoInfoOptions, VideoInfoResult };\n\n// Import the native module. On web, it will be resolved to ExpoVideoMetadata.web.ts\n// and on native platforms to ExpoVideoMetadata.ts\n\n/**\n * Retrieves video metadata.\n *\n * @param source An URI (string) of the video, local or remote. On web, it can be a File or Blob object, too. base64 URIs are supported but not recommended, as they can be very large and cause performance issues.\n * @param options Pass `headers` object in case `sourceFilename` is a remote URI, e.g { headers: \"Authorization\": \"Bearer some-token\" } etc.\n *\n * @return Returns a promise which fulfils with [`VideoInfoResult`](#Videoinforesult).\n */\nexport async function getVideoInfoAsync(\n source: VideoSource,\n options: VideoInfoOptions = {}\n): Promise<VideoInfoResult> {\n return await ExpoVideoMetadataModule.getVideoInfo(source, options);\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.0",
3
+ "version": "1.3.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",
@@ -62,8 +62,19 @@ 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
 
76
+ export type VideoSource = string | File | Blob;
77
+
67
78
  export type VideoInfoOptions = {
68
79
  /**
69
80
  * In case `sourceFilename` is a remote URI, `headers` object is passed in a network request.
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  VideoInfoOptions,
3
3
  VideoInfoResult,
4
+ VideoSource,
4
5
  } from "./ExpoVideoMetadata.types";
5
6
 
6
7
  interface Track {
@@ -97,19 +98,16 @@ export default {
97
98
  },
98
99
 
99
100
  async getVideoInfo(
100
- sourceFilename: string | File | Blob,
101
+ source: VideoSource,
101
102
  options: VideoInfoOptions = {}
102
103
  ): Promise<VideoInfoResult> {
103
104
  const video = document.createElement("video") as HTMLVideoElementWithTracks;
104
105
  let videoUrl = "";
105
106
 
106
- if (typeof sourceFilename === "string") {
107
- videoUrl = sourceFilename;
108
- } else if (
109
- sourceFilename instanceof File ||
110
- sourceFilename instanceof Blob
111
- ) {
112
- videoUrl = URL.createObjectURL(sourceFilename);
107
+ if (typeof source === "string") {
108
+ videoUrl = source;
109
+ } else if (source instanceof File || source instanceof Blob) {
110
+ videoUrl = URL.createObjectURL(source);
113
111
  }
114
112
 
115
113
  Object.assign(video, {
@@ -124,6 +122,10 @@ export default {
124
122
  video.removeAttribute("src");
125
123
  video.load();
126
124
  video.remove();
125
+ // Revoke the object URL if it was created
126
+ if (source instanceof File || source instanceof Blob) {
127
+ URL.revokeObjectURL(videoUrl);
128
+ }
127
129
  };
128
130
 
129
131
  try {
@@ -147,8 +149,8 @@ export default {
147
149
  Boolean(video.webkitAudioDecodedByteCount);
148
150
 
149
151
  const fileSize =
150
- sourceFilename instanceof File || sourceFilename instanceof Blob
151
- ? sourceFilename.size
152
+ source instanceof File || source instanceof Blob
153
+ ? source.size
152
154
  : await this.getFileSize(videoUrl, options);
153
155
 
154
156
  const bitRate =
@@ -167,18 +169,16 @@ export default {
167
169
  fileSize,
168
170
  hasAudio,
169
171
  audioSampleRate,
170
- isHDR: null,
172
+ isHDR: null, // not supported on web
171
173
  audioCodec: audioTrack?.label ?? "",
172
174
  codec: videoTrack?.label ?? "",
173
175
  audioChannels,
174
176
  fps,
175
177
  orientation: width >= height ? "Landscape" : "Portrait",
178
+ location: null, // not supported on web
176
179
  };
177
180
  } finally {
178
181
  resetVideo();
179
- if (sourceFilename instanceof File || sourceFilename instanceof Blob) {
180
- URL.revokeObjectURL(videoUrl);
181
- }
182
182
  }
183
183
  },
184
184
  };
package/src/index.ts CHANGED
@@ -1,4 +1,8 @@
1
- import { VideoInfoOptions, VideoInfoResult } from "./ExpoVideoMetadata.types";
1
+ import {
2
+ VideoInfoOptions,
3
+ VideoInfoResult,
4
+ VideoSource,
5
+ } from "./ExpoVideoMetadata.types";
2
6
  import ExpoVideoMetadataModule from "./ExpoVideoMetadataModule";
3
7
 
4
8
  export { VideoInfoOptions, VideoInfoResult };
@@ -9,14 +13,14 @@ export { VideoInfoOptions, VideoInfoResult };
9
13
  /**
10
14
  * Retrieves video metadata.
11
15
  *
12
- * @param sourceFilename An URI of the video, local or remote.
16
+ * @param source An URI (string) of the video, local or remote. On web, it can be a File or Blob object, too. base64 URIs are supported but not recommended, as they can be very large and cause performance issues.
13
17
  * @param options Pass `headers` object in case `sourceFilename` is a remote URI, e.g { headers: "Authorization": "Bearer some-token" } etc.
14
18
  *
15
19
  * @return Returns a promise which fulfils with [`VideoInfoResult`](#Videoinforesult).
16
20
  */
17
21
  export async function getVideoInfoAsync(
18
- sourceFilename: string | File | Blob,
22
+ source: VideoSource,
19
23
  options: VideoInfoOptions = {}
20
24
  ): Promise<VideoInfoResult> {
21
- return await ExpoVideoMetadataModule.getVideoInfo(sourceFilename, options);
25
+ return await ExpoVideoMetadataModule.getVideoInfo(source, options);
22
26
  }