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 +14 -3
- package/android/src/main/java/expo/modules/videometadata/ExpoVideoMetadataModule.kt +43 -1
- package/build/ExpoVideoMetadata.types.d.ts +10 -0
- package/build/ExpoVideoMetadata.types.d.ts.map +1 -1
- package/build/ExpoVideoMetadata.types.js.map +1 -1
- package/build/ExpoVideoMetadataModule.web.d.ts +2 -2
- package/build/ExpoVideoMetadataModule.web.d.ts.map +1 -1
- package/build/ExpoVideoMetadataModule.web.js +13 -12
- package/build/ExpoVideoMetadataModule.web.js.map +1 -1
- package/build/index.d.ts +3 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -3
- package/build/index.js.map +1 -1
- package/ios/ExpoVideoMetadataModule.swift +68 -26
- package/package.json +1 -1
- package/src/ExpoVideoMetadata.types.ts +11 -0
- package/src/ExpoVideoMetadataModule.web.ts +14 -14
- package/src/index.ts +8 -4
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/
|
|
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;
|
|
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(
|
|
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,
|
|
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(
|
|
51
|
+
async getVideoInfo(source, options = {}) {
|
|
52
52
|
const video = document.createElement("video");
|
|
53
53
|
let videoUrl = "";
|
|
54
|
-
if (typeof
|
|
55
|
-
videoUrl =
|
|
54
|
+
if (typeof source === "string") {
|
|
55
|
+
videoUrl = source;
|
|
56
56
|
}
|
|
57
|
-
else if (
|
|
58
|
-
|
|
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 =
|
|
89
|
-
?
|
|
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":"
|
|
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
|
|
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(
|
|
11
|
+
export declare function getVideoInfoAsync(source: VideoSource, options?: VideoInfoOptions): Promise<VideoInfoResult>;
|
|
12
12
|
//# sourceMappingURL=index.d.ts.map
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
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
|
|
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(
|
|
13
|
-
return await ExpoVideoMetadataModule.getVideoInfo(
|
|
12
|
+
export async function getVideoInfoAsync(source, options = {}) {
|
|
13
|
+
return await ExpoVideoMetadataModule.getVideoInfo(source, options);
|
|
14
14
|
}
|
|
15
15
|
//# sourceMappingURL=index.js.map
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
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)
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
@@ -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
|
-
|
|
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
|
|
107
|
-
videoUrl =
|
|
108
|
-
} else if (
|
|
109
|
-
|
|
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
|
-
|
|
151
|
-
?
|
|
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 {
|
|
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
|
|
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
|
-
|
|
22
|
+
source: VideoSource,
|
|
19
23
|
options: VideoInfoOptions = {}
|
|
20
24
|
): Promise<VideoInfoResult> {
|
|
21
|
-
return await ExpoVideoMetadataModule.getVideoInfo(
|
|
25
|
+
return await ExpoVideoMetadataModule.getVideoInfo(source, options);
|
|
22
26
|
}
|