expo-video-metadata 1.0.0 → 1.2.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/.eslintrc.js +5 -2
- package/.prettierrc.json +3 -0
- package/README.md +2 -7
- package/android/src/main/java/expo/modules/videometadata/ExpoVideoMetadataModule.kt +8 -0
- package/build/ExpoVideoMetadata.types.d.ts +11 -1
- package/build/ExpoVideoMetadata.types.d.ts.map +1 -1
- package/build/ExpoVideoMetadata.types.js.map +1 -1
- package/build/ExpoVideoMetadataModule.web.d.ts +29 -2
- package/build/ExpoVideoMetadataModule.web.d.ts.map +1 -1
- package/build/ExpoVideoMetadataModule.web.js +113 -4
- package/build/ExpoVideoMetadataModule.web.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js.map +1 -1
- package/ios/ExpoVideoMetadataModule.swift +7 -0
- package/package.json +5 -5
- package/src/ExpoVideoMetadata.types.ts +11 -0
- package/src/ExpoVideoMetadataModule.web.ts +174 -6
- package/src/index.ts +1 -1
- package/tea.yaml +6 -0
package/.eslintrc.js
CHANGED
package/.prettierrc.json
ADDED
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# expo-video-metadata
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This React Native (Expo) library provides a versatile function designed to extract a wide range of metadata from video files, including duration, width, height, frame rate, codec, audio availability, orientation, audio channels, audio codec, and audio sample rate. For comprehensive details, please refer to the listed exported types. Although the library is equipped with web support, its performance is reliant on specific platform APIs, leading to potential variability in its effectiveness across different browsers.
|
|
4
4
|
|
|
5
5
|
<img src="https://raw.githubusercontent.com/hirbod/expo-video-metadata/assets/preview.png" width="500" />
|
|
6
6
|
|
|
@@ -38,12 +38,7 @@ import { getVideoInfoAsync } from 'expo-video-metadata';
|
|
|
38
38
|
* @return Returns a promise which fulfils with [`VideoInfoResult`](#Videoinforesult).
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
-
const result = await getVideoInfoAsync(
|
|
42
|
-
sourceFilename: string,
|
|
43
|
-
options: VideoInfoOptions = {},
|
|
44
|
-
): Promise<VideoInfoResult> {
|
|
45
|
-
return await ExpoVideoMetadataModule.getVideoInfo(sourceFilename, options);
|
|
46
|
-
}
|
|
41
|
+
const result = await getVideoInfoAsync(sourceFilename: string, options: VideoInfoOptions = {}): Promise<VideoInfoResult>
|
|
47
42
|
```
|
|
48
43
|
|
|
49
44
|
See [VideoInfoResult](https://github.com/hirbod/expo-video-metadata/blob/b9239224eed46f455b2fb9f1b29e69ac49da6683/src/ExpoVideoMetadata.types.ts#L1) type for more information.
|
|
@@ -17,6 +17,7 @@ import kotlinx.coroutines.cancel
|
|
|
17
17
|
import kotlinx.coroutines.launch
|
|
18
18
|
import java.io.File
|
|
19
19
|
import android.media.MediaExtractor
|
|
20
|
+
import android.media.MediaFeature
|
|
20
21
|
import android.media.MediaFormat
|
|
21
22
|
import java.math.BigDecimal
|
|
22
23
|
import java.math.RoundingMode
|
|
@@ -69,6 +70,12 @@ class ExpoVideoMetadataModule : Module() {
|
|
|
69
70
|
val rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toIntOrNull()
|
|
70
71
|
val hasAudio = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO) != null
|
|
71
72
|
|
|
73
|
+
val colorTransfer = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COLOR_TRANSFER)?.toIntOrNull()
|
|
74
|
+
var isHDR: Boolean? = null
|
|
75
|
+
if (colorTransfer != null) {
|
|
76
|
+
isHDR = colorTransfer == MediaFormat.COLOR_TRANSFER_ST2084 || colorTransfer == MediaFormat.COLOR_TRANSFER_HLG
|
|
77
|
+
}
|
|
78
|
+
|
|
72
79
|
// release
|
|
73
80
|
retriever.release()
|
|
74
81
|
|
|
@@ -115,6 +122,7 @@ class ExpoVideoMetadataModule : Module() {
|
|
|
115
122
|
"bitrate" to bitrate,
|
|
116
123
|
"fileSize" to fileSize,
|
|
117
124
|
"hasAudio" to hasAudio,
|
|
125
|
+
"isHDR" to isHDR,
|
|
118
126
|
"audioCodec" to audioCodec,
|
|
119
127
|
"orientation" to getOrientation(rotation),
|
|
120
128
|
"audioSampleRate" to audioSampleRate,
|
|
@@ -7,6 +7,11 @@ export type VideoInfoResult = {
|
|
|
7
7
|
* Tells if the video has a audio track. If the video has no audio track, its considered a mute video.
|
|
8
8
|
*/
|
|
9
9
|
hasAudio: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Available only on iOS >= 14 and Android. Tells if the video is a HDR video.
|
|
12
|
+
* Will return null if it could not be determined. (e.g. on Web or on older iOS/Android versions)
|
|
13
|
+
*/
|
|
14
|
+
isHDR: boolean | null;
|
|
10
15
|
/**
|
|
11
16
|
* Width of the video in pixels.
|
|
12
17
|
*/
|
|
@@ -17,24 +22,29 @@ export type VideoInfoResult = {
|
|
|
17
22
|
height: number;
|
|
18
23
|
/**
|
|
19
24
|
* Frame rate of the video in frames per second.
|
|
25
|
+
* Works on iOS, Android and Web (except Safari).
|
|
20
26
|
*/
|
|
21
27
|
fps: number;
|
|
22
28
|
/**
|
|
23
29
|
* Bit rate of the video in bits per second.
|
|
30
|
+
* Supported on all platforms.
|
|
24
31
|
*/
|
|
25
32
|
bitRate: number;
|
|
26
33
|
/**
|
|
27
34
|
* File size of the video in bytes. Works only for local files, returns 0 for remote files.
|
|
35
|
+
* Supported on all platforms.
|
|
28
36
|
*/
|
|
29
37
|
fileSize: number;
|
|
30
38
|
/**
|
|
31
39
|
* Video codec.
|
|
40
|
+
* Supported on all platforms, but on Web it may return an empty string.
|
|
32
41
|
*/
|
|
33
42
|
codec: string;
|
|
34
43
|
/**
|
|
35
44
|
* Video orientation.
|
|
45
|
+
* Supported on all platforms, but on Web it may return an empty string.
|
|
36
46
|
*/
|
|
37
|
-
orientation: "Portrait" | "PortraitUpsideDown" | "LandscapeRight" | "LandscapeLeft";
|
|
47
|
+
orientation: "Portrait" | "PortraitUpsideDown" | "Landscape" | "LandscapeRight" | "LandscapeLeft";
|
|
38
48
|
/**
|
|
39
49
|
* Audio sample rate of the video in samples per second.
|
|
40
50
|
*/
|
|
@@ -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;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf
|
|
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 +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 * 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 */\n fps: number;\n /**\n * Bit rate of the video in bits per second.\n */\n bitRate: number;\n /**\n * File size of the video in bytes. Works only for local files, returns 0 for remote files.\n */\n fileSize: number;\n /**\n * Video codec.\n */\n codec: string;\n /**\n * Video orientation.\n */\n orientation:\n | \"Portrait\"\n | \"PortraitUpsideDown\"\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\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,7 +1,34 @@
|
|
|
1
1
|
import type { VideoInfoOptions, VideoInfoResult } from "./ExpoVideoMetadata.types";
|
|
2
|
+
interface Track {
|
|
3
|
+
id: string;
|
|
4
|
+
kind: string;
|
|
5
|
+
label: string;
|
|
6
|
+
language: string;
|
|
7
|
+
}
|
|
8
|
+
interface AudioTrack extends Track {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface VideoTrack extends Track {
|
|
12
|
+
selected: boolean;
|
|
13
|
+
}
|
|
14
|
+
type TrackList<T> = T[];
|
|
15
|
+
interface HTMLVideoElementWithTracks extends HTMLVideoElement {
|
|
16
|
+
videoTracks?: TrackList<VideoTrack>;
|
|
17
|
+
audioTracks?: TrackList<AudioTrack>;
|
|
18
|
+
mozHasAudio?: boolean;
|
|
19
|
+
webkitAudioDecodedByteCount?: number;
|
|
20
|
+
captureStream?(): MediaStream;
|
|
21
|
+
}
|
|
2
22
|
declare const _default: {
|
|
3
|
-
|
|
4
|
-
|
|
23
|
+
name: string;
|
|
24
|
+
getVideoFrameRate(videoElement: HTMLVideoElementWithTracks): number;
|
|
25
|
+
getAudioBuffer(audioUrl: string, options?: RequestInit): Promise<{
|
|
26
|
+
sampleRate: number;
|
|
27
|
+
numberOfChannels: number;
|
|
28
|
+
}>;
|
|
29
|
+
getBase64FileSize(base64String: string): number;
|
|
30
|
+
getFileSize(url: string, options?: RequestInit): Promise<number>;
|
|
31
|
+
getVideoInfo(sourceFilename: string | File | Blob, options?: VideoInfoOptions): Promise<VideoInfoResult>;
|
|
5
32
|
};
|
|
6
33
|
export default _default;
|
|
7
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;;;
|
|
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,9 +1,118 @@
|
|
|
1
1
|
export default {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
name: "ExpoVideoMetadata",
|
|
3
|
+
getVideoFrameRate(videoElement) {
|
|
4
|
+
if (!videoElement.captureStream) {
|
|
5
|
+
console.info("captureStream method not supported");
|
|
6
|
+
return 0;
|
|
7
|
+
}
|
|
8
|
+
const stream = videoElement.captureStream();
|
|
9
|
+
const [videoTrack] = stream.getVideoTracks();
|
|
10
|
+
if (!videoTrack) {
|
|
11
|
+
console.info("No video track found");
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
return videoTrack.getSettings().frameRate ?? 0;
|
|
4
15
|
},
|
|
5
|
-
async
|
|
6
|
-
|
|
16
|
+
async getAudioBuffer(audioUrl, options = {}) {
|
|
17
|
+
const audioContext = new (window.AudioContext ||
|
|
18
|
+
window.webkitAudioContext)();
|
|
19
|
+
try {
|
|
20
|
+
const response = await fetch(audioUrl, options);
|
|
21
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
audioContext.decodeAudioData(arrayBuffer, ({ sampleRate, numberOfChannels }) => resolve({ sampleRate, numberOfChannels }), reject);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
throw new Error(`Error fetching or decoding audio file: ${error.message}`);
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
await audioContext.close();
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
getBase64FileSize(base64String) {
|
|
34
|
+
const base64Data = base64String.replace(/^data:.+;base64,/, "");
|
|
35
|
+
return atob(base64Data).length;
|
|
36
|
+
},
|
|
37
|
+
async getFileSize(url, options = {}) {
|
|
38
|
+
if (url.startsWith("data:")) {
|
|
39
|
+
return this.getBase64FileSize(url);
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetch(url, { method: "HEAD", ...options });
|
|
43
|
+
const contentLength = response.headers.get("Content-Length");
|
|
44
|
+
return contentLength ? parseInt(contentLength, 10) : 0;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.error("Error fetching file size for URL:", url, error);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
async getVideoInfo(sourceFilename, options = {}) {
|
|
52
|
+
const video = document.createElement("video");
|
|
53
|
+
let videoUrl = "";
|
|
54
|
+
if (typeof sourceFilename === "string") {
|
|
55
|
+
videoUrl = sourceFilename;
|
|
56
|
+
}
|
|
57
|
+
else if (sourceFilename instanceof File ||
|
|
58
|
+
sourceFilename instanceof Blob) {
|
|
59
|
+
videoUrl = URL.createObjectURL(sourceFilename);
|
|
60
|
+
}
|
|
61
|
+
Object.assign(video, {
|
|
62
|
+
crossOrigin: "anonymous",
|
|
63
|
+
autoplay: true,
|
|
64
|
+
muted: true,
|
|
65
|
+
playsInline: true,
|
|
66
|
+
});
|
|
67
|
+
const resetVideo = () => {
|
|
68
|
+
video.pause();
|
|
69
|
+
video.removeAttribute("src");
|
|
70
|
+
video.load();
|
|
71
|
+
video.remove();
|
|
72
|
+
};
|
|
73
|
+
try {
|
|
74
|
+
await new Promise((resolve, reject) => {
|
|
75
|
+
// Can't use `loadedmetadata` event because it does not contain videoTracks, audioTracks and other metadata
|
|
76
|
+
video.onloadeddata = () => resolve();
|
|
77
|
+
video.onerror = () => reject(new Error("Failed to load video metadata"));
|
|
78
|
+
video.src = videoUrl;
|
|
79
|
+
video.load();
|
|
80
|
+
video.pause();
|
|
81
|
+
});
|
|
82
|
+
const { duration, videoWidth: width, videoHeight: height } = video;
|
|
83
|
+
const videoTrack = video.videoTracks?.[0];
|
|
84
|
+
const audioTrack = video.audioTracks?.[0];
|
|
85
|
+
const hasAudio = Boolean(video.audioTracks?.length) ||
|
|
86
|
+
video.mozHasAudio ||
|
|
87
|
+
Boolean(video.webkitAudioDecodedByteCount);
|
|
88
|
+
const fileSize = sourceFilename instanceof File || sourceFilename instanceof Blob
|
|
89
|
+
? sourceFilename.size
|
|
90
|
+
: await this.getFileSize(videoUrl, options);
|
|
91
|
+
const bitRate = fileSize && duration ? Math.floor(fileSize / duration) : 0;
|
|
92
|
+
const { numberOfChannels: audioChannels, sampleRate: audioSampleRate } = await this.getAudioBuffer(videoUrl);
|
|
93
|
+
const fps = this.getVideoFrameRate(video);
|
|
94
|
+
return {
|
|
95
|
+
duration,
|
|
96
|
+
width,
|
|
97
|
+
height,
|
|
98
|
+
bitRate,
|
|
99
|
+
fileSize,
|
|
100
|
+
hasAudio,
|
|
101
|
+
audioSampleRate,
|
|
102
|
+
isHDR: null,
|
|
103
|
+
audioCodec: audioTrack?.label ?? "",
|
|
104
|
+
codec: videoTrack?.label ?? "",
|
|
105
|
+
audioChannels,
|
|
106
|
+
fps,
|
|
107
|
+
orientation: width >= height ? "Landscape" : "Portrait",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
resetVideo();
|
|
112
|
+
if (sourceFilename instanceof File || sourceFilename instanceof Blob) {
|
|
113
|
+
URL.revokeObjectURL(videoUrl);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
7
116
|
},
|
|
8
117
|
};
|
|
9
118
|
//# sourceMappingURL=ExpoVideoMetadataModule.web.js.map
|
|
@@ -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} 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"]}
|
package/build/index.d.ts
CHANGED
|
@@ -8,5 +8,5 @@ export { VideoInfoOptions, VideoInfoResult };
|
|
|
8
8
|
*
|
|
9
9
|
* @return Returns a promise which fulfils with [`VideoInfoResult`](#Videoinforesult).
|
|
10
10
|
*/
|
|
11
|
-
export declare function getVideoInfoAsync(sourceFilename: string, options?: VideoInfoOptions): Promise<VideoInfoResult>;
|
|
11
|
+
export declare function getVideoInfoAsync(sourceFilename: string | File | Blob, 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,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,
|
|
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"}
|
package/build/index.js.map
CHANGED
|
@@ -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,
|
|
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"]}
|
|
@@ -30,6 +30,7 @@ public class ExpoVideoMetadataModule: Module {
|
|
|
30
30
|
var width: Int = 0
|
|
31
31
|
var height: Int = 0
|
|
32
32
|
var frameRate: Float = 0.0
|
|
33
|
+
var isHDR: Bool? = nil
|
|
33
34
|
var codec: String = ""
|
|
34
35
|
var orientation: String = ""
|
|
35
36
|
var audioSampleRate: Int = 0
|
|
@@ -63,6 +64,11 @@ public class ExpoVideoMetadataModule: Module {
|
|
|
63
64
|
} else {
|
|
64
65
|
orientation = (transform.a == 1.0) ? "LandscapeRight" : "LandscapeLeft"
|
|
65
66
|
}
|
|
67
|
+
|
|
68
|
+
// HDR
|
|
69
|
+
if #available(iOS 14.0, *) {
|
|
70
|
+
isHDR = videoTrack.hasMediaCharacteristic(.containsHDRVideo)
|
|
71
|
+
}
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
// Audio track information
|
|
@@ -84,6 +90,7 @@ public class ExpoVideoMetadataModule: Module {
|
|
|
84
90
|
return [
|
|
85
91
|
"duration": duration,
|
|
86
92
|
"hasAudio": hasAudio,
|
|
93
|
+
"isHDR": isHDR as Any,
|
|
87
94
|
"fileSize": fileSize,
|
|
88
95
|
"bitrate": bitrate,
|
|
89
96
|
"fps": frameRate,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-video-metadata",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Module to retrieve metadata from a video with Expo and React Native",
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|
|
7
7
|
"scripts": {
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"homepage": "https://github.com/hirbod/expo-video-metadata#readme",
|
|
36
36
|
"dependencies": {},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@types/react": "^18.
|
|
39
|
-
"expo-module-scripts": "^3.
|
|
40
|
-
"expo-modules-core": "^1.
|
|
38
|
+
"@types/react": "^18.2.45",
|
|
39
|
+
"expo-module-scripts": "^3.5.2",
|
|
40
|
+
"expo-modules-core": "^1.12.18"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"expo": "*",
|
|
@@ -7,6 +7,11 @@ export type VideoInfoResult = {
|
|
|
7
7
|
* Tells if the video has a audio track. If the video has no audio track, its considered a mute video.
|
|
8
8
|
*/
|
|
9
9
|
hasAudio: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Available only on iOS >= 14 and Android. Tells if the video is a HDR video.
|
|
12
|
+
* Will return null if it could not be determined. (e.g. on Web or on older iOS/Android versions)
|
|
13
|
+
*/
|
|
14
|
+
isHDR: boolean | null;
|
|
10
15
|
/**
|
|
11
16
|
* Width of the video in pixels.
|
|
12
17
|
*/
|
|
@@ -17,26 +22,32 @@ export type VideoInfoResult = {
|
|
|
17
22
|
height: number;
|
|
18
23
|
/**
|
|
19
24
|
* Frame rate of the video in frames per second.
|
|
25
|
+
* Works on iOS, Android and Web (except Safari).
|
|
20
26
|
*/
|
|
21
27
|
fps: number;
|
|
22
28
|
/**
|
|
23
29
|
* Bit rate of the video in bits per second.
|
|
30
|
+
* Supported on all platforms.
|
|
24
31
|
*/
|
|
25
32
|
bitRate: number;
|
|
26
33
|
/**
|
|
27
34
|
* File size of the video in bytes. Works only for local files, returns 0 for remote files.
|
|
35
|
+
* Supported on all platforms.
|
|
28
36
|
*/
|
|
29
37
|
fileSize: number;
|
|
30
38
|
/**
|
|
31
39
|
* Video codec.
|
|
40
|
+
* Supported on all platforms, but on Web it may return an empty string.
|
|
32
41
|
*/
|
|
33
42
|
codec: string;
|
|
34
43
|
/**
|
|
35
44
|
* Video orientation.
|
|
45
|
+
* Supported on all platforms, but on Web it may return an empty string.
|
|
36
46
|
*/
|
|
37
47
|
orientation:
|
|
38
48
|
| "Portrait"
|
|
39
49
|
| "PortraitUpsideDown"
|
|
50
|
+
| "Landscape"
|
|
40
51
|
| "LandscapeRight"
|
|
41
52
|
| "LandscapeLeft";
|
|
42
53
|
/**
|
|
@@ -3,14 +3,182 @@ import type {
|
|
|
3
3
|
VideoInfoResult,
|
|
4
4
|
} from "./ExpoVideoMetadata.types";
|
|
5
5
|
|
|
6
|
+
interface Track {
|
|
7
|
+
id: string;
|
|
8
|
+
kind: string;
|
|
9
|
+
label: string;
|
|
10
|
+
language: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface AudioTrack extends Track {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface VideoTrack extends Track {
|
|
18
|
+
selected: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type TrackList<T> = T[];
|
|
22
|
+
|
|
23
|
+
interface HTMLVideoElementWithTracks extends HTMLVideoElement {
|
|
24
|
+
videoTracks?: TrackList<VideoTrack>;
|
|
25
|
+
audioTracks?: TrackList<AudioTrack>;
|
|
26
|
+
mozHasAudio?: boolean;
|
|
27
|
+
webkitAudioDecodedByteCount?: number;
|
|
28
|
+
captureStream?(): MediaStream;
|
|
29
|
+
}
|
|
30
|
+
|
|
6
31
|
export default {
|
|
7
|
-
|
|
8
|
-
|
|
32
|
+
name: "ExpoVideoMetadata",
|
|
33
|
+
|
|
34
|
+
getVideoFrameRate(videoElement: HTMLVideoElementWithTracks) {
|
|
35
|
+
if (!videoElement.captureStream) {
|
|
36
|
+
console.info("captureStream method not supported");
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const stream = videoElement.captureStream();
|
|
41
|
+
const [videoTrack] = stream.getVideoTracks();
|
|
42
|
+
|
|
43
|
+
if (!videoTrack) {
|
|
44
|
+
console.info("No video track found");
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return videoTrack.getSettings().frameRate ?? 0;
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
async getAudioBuffer(
|
|
52
|
+
audioUrl: string,
|
|
53
|
+
options: RequestInit = {}
|
|
54
|
+
): Promise<{ sampleRate: number; numberOfChannels: number }> {
|
|
55
|
+
const audioContext = new (window.AudioContext ||
|
|
56
|
+
(window as any).webkitAudioContext)();
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(audioUrl, options);
|
|
60
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
61
|
+
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
audioContext.decodeAudioData(
|
|
64
|
+
arrayBuffer,
|
|
65
|
+
({ sampleRate, numberOfChannels }) =>
|
|
66
|
+
resolve({ sampleRate, numberOfChannels }),
|
|
67
|
+
reject
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Error fetching or decoding audio file: ${error.message}`
|
|
73
|
+
);
|
|
74
|
+
} finally {
|
|
75
|
+
await audioContext.close();
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
getBase64FileSize(base64String: string): number {
|
|
80
|
+
const base64Data = base64String.replace(/^data:.+;base64,/, "");
|
|
81
|
+
return atob(base64Data).length;
|
|
9
82
|
},
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
83
|
+
|
|
84
|
+
async getFileSize(url: string, options: RequestInit = {}): Promise<number> {
|
|
85
|
+
if (url.startsWith("data:")) {
|
|
86
|
+
return this.getBase64FileSize(url);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const response = await fetch(url, { method: "HEAD", ...options });
|
|
91
|
+
const contentLength = response.headers.get("Content-Length");
|
|
92
|
+
return contentLength ? parseInt(contentLength, 10) : 0;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error("Error fetching file size for URL:", url, error);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
async getVideoInfo(
|
|
100
|
+
sourceFilename: string | File | Blob,
|
|
101
|
+
options: VideoInfoOptions = {}
|
|
13
102
|
): Promise<VideoInfoResult> {
|
|
14
|
-
|
|
103
|
+
const video = document.createElement("video") as HTMLVideoElementWithTracks;
|
|
104
|
+
let videoUrl = "";
|
|
105
|
+
|
|
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);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
Object.assign(video, {
|
|
116
|
+
crossOrigin: "anonymous",
|
|
117
|
+
autoplay: true,
|
|
118
|
+
muted: true,
|
|
119
|
+
playsInline: true,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const resetVideo = () => {
|
|
123
|
+
video.pause();
|
|
124
|
+
video.removeAttribute("src");
|
|
125
|
+
video.load();
|
|
126
|
+
video.remove();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
await new Promise<void>((resolve, reject) => {
|
|
131
|
+
// Can't use `loadedmetadata` event because it does not contain videoTracks, audioTracks and other metadata
|
|
132
|
+
video.onloadeddata = () => resolve();
|
|
133
|
+
video.onerror = () =>
|
|
134
|
+
reject(new Error("Failed to load video metadata"));
|
|
135
|
+
video.src = videoUrl;
|
|
136
|
+
video.load();
|
|
137
|
+
video.pause();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const { duration, videoWidth: width, videoHeight: height } = video;
|
|
141
|
+
const videoTrack = video.videoTracks?.[0];
|
|
142
|
+
const audioTrack = video.audioTracks?.[0];
|
|
143
|
+
|
|
144
|
+
const hasAudio =
|
|
145
|
+
Boolean(video.audioTracks?.length) ||
|
|
146
|
+
video.mozHasAudio ||
|
|
147
|
+
Boolean(video.webkitAudioDecodedByteCount);
|
|
148
|
+
|
|
149
|
+
const fileSize =
|
|
150
|
+
sourceFilename instanceof File || sourceFilename instanceof Blob
|
|
151
|
+
? sourceFilename.size
|
|
152
|
+
: await this.getFileSize(videoUrl, options);
|
|
153
|
+
|
|
154
|
+
const bitRate =
|
|
155
|
+
fileSize && duration ? Math.floor(fileSize / duration) : 0;
|
|
156
|
+
|
|
157
|
+
const { numberOfChannels: audioChannels, sampleRate: audioSampleRate } =
|
|
158
|
+
await this.getAudioBuffer(videoUrl);
|
|
159
|
+
|
|
160
|
+
const fps = this.getVideoFrameRate(video);
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
duration,
|
|
164
|
+
width,
|
|
165
|
+
height,
|
|
166
|
+
bitRate,
|
|
167
|
+
fileSize,
|
|
168
|
+
hasAudio,
|
|
169
|
+
audioSampleRate,
|
|
170
|
+
isHDR: null,
|
|
171
|
+
audioCodec: audioTrack?.label ?? "",
|
|
172
|
+
codec: videoTrack?.label ?? "",
|
|
173
|
+
audioChannels,
|
|
174
|
+
fps,
|
|
175
|
+
orientation: width >= height ? "Landscape" : "Portrait",
|
|
176
|
+
};
|
|
177
|
+
} finally {
|
|
178
|
+
resetVideo();
|
|
179
|
+
if (sourceFilename instanceof File || sourceFilename instanceof Blob) {
|
|
180
|
+
URL.revokeObjectURL(videoUrl);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
15
183
|
},
|
|
16
184
|
};
|
package/src/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ export { VideoInfoOptions, VideoInfoResult };
|
|
|
15
15
|
* @return Returns a promise which fulfils with [`VideoInfoResult`](#Videoinforesult).
|
|
16
16
|
*/
|
|
17
17
|
export async function getVideoInfoAsync(
|
|
18
|
-
sourceFilename: string,
|
|
18
|
+
sourceFilename: string | File | Blob,
|
|
19
19
|
options: VideoInfoOptions = {}
|
|
20
20
|
): Promise<VideoInfoResult> {
|
|
21
21
|
return await ExpoVideoMetadataModule.getVideoInfo(sourceFilename, options);
|