expo-video-metadata 1.1.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/README.md +1 -1
- package/android/src/main/java/expo/modules/videometadata/ExpoVideoMetadataModule.kt +8 -0
- 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 +25 -5
- package/build/ExpoVideoMetadataModule.web.d.ts.map +1 -1
- package/build/ExpoVideoMetadataModule.web.js +80 -109
- 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 +3 -3
- package/src/ExpoVideoMetadata.types.ts +10 -0
- package/src/ExpoVideoMetadataModule.web.ts +117 -144
- package/src/index.ts +1 -1
- package/tea.yaml +6 -0
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
|
|
|
@@ -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,22 +22,27 @@ 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: "Portrait" | "PortraitUpsideDown" | "Landscape" | "LandscapeRight" | "LandscapeLeft";
|
|
38
48
|
/**
|
|
@@ -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 | \"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\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,14 +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
|
-
getVideoFrameRate(videoElement:
|
|
5
|
-
getAudioBuffer(audioUrl: string, options?:
|
|
23
|
+
name: string;
|
|
24
|
+
getVideoFrameRate(videoElement: HTMLVideoElementWithTracks): number;
|
|
25
|
+
getAudioBuffer(audioUrl: string, options?: RequestInit): Promise<{
|
|
6
26
|
sampleRate: number;
|
|
7
27
|
numberOfChannels: number;
|
|
8
28
|
}>;
|
|
9
29
|
getBase64FileSize(base64String: string): number;
|
|
10
|
-
getFileSize(url: string, options?:
|
|
11
|
-
getVideoInfo(sourceFilename: string, options?: VideoInfoOptions): Promise<VideoInfoResult>;
|
|
30
|
+
getFileSize(url: string, options?: RequestInit): Promise<number>;
|
|
31
|
+
getVideoInfo(sourceFilename: string | File | Blob, options?: VideoInfoOptions): Promise<VideoInfoResult>;
|
|
12
32
|
};
|
|
13
33
|
export default _default;
|
|
14
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,147 +1,118 @@
|
|
|
1
1
|
export default {
|
|
2
|
-
|
|
3
|
-
return "ExpoVideoMetadata";
|
|
4
|
-
},
|
|
2
|
+
name: "ExpoVideoMetadata",
|
|
5
3
|
getVideoFrameRate(videoElement) {
|
|
6
4
|
if (!videoElement.captureStream) {
|
|
7
|
-
console.
|
|
5
|
+
console.info("captureStream method not supported");
|
|
8
6
|
return 0;
|
|
9
7
|
}
|
|
10
8
|
const stream = videoElement.captureStream();
|
|
11
|
-
const videoTrack = stream.getVideoTracks()
|
|
9
|
+
const [videoTrack] = stream.getVideoTracks();
|
|
12
10
|
if (!videoTrack) {
|
|
13
|
-
console.
|
|
11
|
+
console.info("No video track found");
|
|
14
12
|
return 0;
|
|
15
13
|
}
|
|
16
|
-
|
|
17
|
-
return settings.frameRate;
|
|
14
|
+
return videoTrack.getSettings().frameRate ?? 0;
|
|
18
15
|
},
|
|
19
16
|
async getAudioBuffer(audioUrl, options = {}) {
|
|
20
17
|
const audioContext = new (window.AudioContext ||
|
|
21
|
-
// @ts-expect-error
|
|
22
18
|
window.webkitAudioContext)();
|
|
23
19
|
try {
|
|
24
20
|
const response = await fetch(audioUrl, options);
|
|
25
21
|
const arrayBuffer = await response.arrayBuffer();
|
|
26
22
|
return new Promise((resolve, reject) => {
|
|
27
|
-
audioContext.decodeAudioData(arrayBuffer, (
|
|
28
|
-
resolve({
|
|
29
|
-
sampleRate: audioBuffer.sampleRate,
|
|
30
|
-
numberOfChannels: audioBuffer.numberOfChannels,
|
|
31
|
-
});
|
|
32
|
-
}, reject);
|
|
23
|
+
audioContext.decodeAudioData(arrayBuffer, ({ sampleRate, numberOfChannels }) => resolve({ sampleRate, numberOfChannels }), reject);
|
|
33
24
|
});
|
|
34
25
|
}
|
|
35
26
|
catch (error) {
|
|
36
|
-
|
|
37
|
-
throw new Error("Error fetching or decoding audio file: " + error.message);
|
|
27
|
+
throw new Error(`Error fetching or decoding audio file: ${error.message}`);
|
|
38
28
|
}
|
|
39
29
|
finally {
|
|
40
|
-
audioContext.close();
|
|
30
|
+
await audioContext.close();
|
|
41
31
|
}
|
|
42
32
|
},
|
|
43
33
|
getBase64FileSize(base64String) {
|
|
44
|
-
// Use a regular expression to remove any data URL prefix
|
|
45
34
|
const base64Data = base64String.replace(/^data:.+;base64,/, "");
|
|
46
|
-
|
|
47
|
-
const binaryString = atob(base64Data);
|
|
48
|
-
// Create an ArrayBuffer with the same length as the binary string
|
|
49
|
-
const bytes = new Uint8Array(binaryString.length);
|
|
50
|
-
// Fill the ArrayBuffer with elements from the binary string
|
|
51
|
-
for (let i = 0; i < binaryString.length; i++) {
|
|
52
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
53
|
-
}
|
|
54
|
-
// The length of the ArrayBuffer is the size in bytes
|
|
55
|
-
return bytes.length;
|
|
35
|
+
return atob(base64Data).length;
|
|
56
36
|
},
|
|
57
37
|
async getFileSize(url, options = {}) {
|
|
58
38
|
if (url.startsWith("data:")) {
|
|
59
|
-
// Handle data URL
|
|
60
39
|
return this.getBase64FileSize(url);
|
|
61
40
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
console.error("Error fetching file size for URL:", url, error);
|
|
71
|
-
return 0;
|
|
72
|
-
}
|
|
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;
|
|
73
49
|
}
|
|
74
50
|
},
|
|
75
51
|
async getVideoInfo(sourceFilename, options = {}) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
videoCodec = video.videoTracks[0].label || ""; // This is not always reliable
|
|
91
|
-
}
|
|
92
|
-
if (video.audioTracks && video.audioTracks.length > 0) {
|
|
93
|
-
audioCodec = video.audioTracks[0].label || ""; // This is not always reliable
|
|
94
|
-
}
|
|
95
|
-
const hasAudio = Boolean(video.audioTracks && video.audioTracks.length) ||
|
|
96
|
-
video.mozHasAudio ||
|
|
97
|
-
Boolean(video.webkitAudioDecodedByteCount);
|
|
98
|
-
let fileSize = 0;
|
|
99
|
-
try {
|
|
100
|
-
fileSize = await this.getFileSize(sourceFilename, options);
|
|
101
|
-
}
|
|
102
|
-
catch (error) {
|
|
103
|
-
console.error("Error fetching file size:", error);
|
|
104
|
-
}
|
|
105
|
-
// just an estimate by formula Bitrate (Bps) = (File Size (in kilobytes) / Duration (in seconds)) * 8
|
|
106
|
-
const bitRate = fileSize && duration ? Math.floor(fileSize / duration) : 0;
|
|
107
|
-
video.remove();
|
|
108
|
-
let audioChannels = 0;
|
|
109
|
-
let audioSampleRate = 0;
|
|
110
|
-
try {
|
|
111
|
-
const audioBuffer = await this.getAudioBuffer(sourceFilename);
|
|
112
|
-
audioChannels = audioBuffer.numberOfChannels;
|
|
113
|
-
audioSampleRate = audioBuffer.sampleRate;
|
|
114
|
-
}
|
|
115
|
-
catch (error) {
|
|
116
|
-
console.error("Error fetching audio buffer:", error);
|
|
117
|
-
}
|
|
118
|
-
let fps = 0;
|
|
119
|
-
try {
|
|
120
|
-
fps = this.getVideoFrameRate(video);
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
// Ignore
|
|
124
|
-
}
|
|
125
|
-
resolve({
|
|
126
|
-
duration,
|
|
127
|
-
width,
|
|
128
|
-
height,
|
|
129
|
-
bitRate,
|
|
130
|
-
fileSize,
|
|
131
|
-
hasAudio,
|
|
132
|
-
audioSampleRate, // Not available
|
|
133
|
-
audioChannels, // Not available
|
|
134
|
-
audioCodec,
|
|
135
|
-
codec: videoCodec,
|
|
136
|
-
fps, // Not available
|
|
137
|
-
orientation: width >= height ? "Landscape" : "Portrait",
|
|
138
|
-
});
|
|
139
|
-
};
|
|
140
|
-
video.onerror = () => {
|
|
141
|
-
reject(new Error("Failed to load video metadata"));
|
|
142
|
-
};
|
|
143
|
-
video.src = sourceFilename; // Set the source of the video
|
|
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,
|
|
144
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
|
+
}
|
|
145
116
|
},
|
|
146
117
|
};
|
|
147
118
|
//# sourceMappingURL=ExpoVideoMetadataModule.web.js.map
|
|
@@ -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\n// Define minimal structures for AudioTrack and VideoTrack based on the specification\ninterface AudioTrack {\n id: string;\n kind: string;\n label: string;\n language: string;\n enabled: boolean;\n}\n\ninterface VideoTrack {\n id: string;\n kind: string;\n label: string;\n language: string;\n selected: boolean;\n}\n\n// Define AudioTrackList and VideoTrackList as arrays of their respective track types\ntype AudioTrackList = AudioTrack[];\ntype VideoTrackList = VideoTrack[];\n\n// Extend the HTMLVideoElement type for browsers that support videoTracks and audioTracks\ninterface HTMLVideoElementWithTracks extends HTMLVideoElement {\n videoTracks?: VideoTrackList;\n audioTracks?: AudioTrackList;\n mozHasAudio?: boolean;\n webkitAudioDecodedByteCount?: number;\n}\n\nexport default {\n get name(): string {\n return \"ExpoVideoMetadata\";\n },\n getVideoFrameRate(videoElement) {\n if (!videoElement.captureStream) {\n console.error(\"captureStream method not supported\");\n return 0;\n }\n\n const stream = videoElement.captureStream();\n const videoTrack = stream.getVideoTracks()[0];\n\n if (!videoTrack) {\n console.error(\"No video track found\");\n return 0;\n }\n\n const settings = videoTrack.getSettings();\n return settings.frameRate;\n },\n async getAudioBuffer(\n audioUrl: string,\n options = {}\n ): Promise<{ sampleRate: number; numberOfChannels: number }> {\n const audioContext = new (window.AudioContext ||\n // @ts-expect-error\n window.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 (audioBuffer) => {\n resolve({\n sampleRate: audioBuffer.sampleRate,\n numberOfChannels: audioBuffer.numberOfChannels,\n });\n },\n reject\n );\n });\n } catch (error) {\n // Handle fetch errors or other errors here\n throw new Error(\n \"Error fetching or decoding audio file: \" + error.message\n );\n } finally {\n audioContext.close();\n }\n },\n getBase64FileSize(base64String: string) {\n // Use a regular expression to remove any data URL prefix\n const base64Data = base64String.replace(/^data:.+;base64,/, \"\");\n\n // Decode the base64 string\n const binaryString = atob(base64Data);\n\n // Create an ArrayBuffer with the same length as the binary string\n const bytes = new Uint8Array(binaryString.length);\n\n // Fill the ArrayBuffer with elements from the binary string\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n\n // The length of the ArrayBuffer is the size in bytes\n return bytes.length;\n },\n async getFileSize(url: string, options = {}) {\n if (url.startsWith(\"data:\")) {\n // Handle data URL\n return this.getBase64FileSize(url);\n } else {\n // Handle regular URL\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,\n options: VideoInfoOptions = {}\n ): Promise<VideoInfoResult> {\n return new Promise((resolve, reject) => {\n const video = document.createElement(\n \"video\"\n ) as HTMLVideoElementWithTracks;\n\n // this is required for Chrome, otherwise it can't detect audio\n video.muted = true;\n video.autoplay = true;\n\n // we need to wait for `loadeddata` instead of `loadedmetadata` because audioTracks and videoTracks are not available until then\n video.onloadeddata = async () => {\n const duration = video.duration; // in seconds\n const width = video.videoWidth;\n const height = video.videoHeight;\n\n // Attempt to extract codec information\n let videoCodec = \"\";\n let audioCodec = \"\";\n if (video.videoTracks && video.videoTracks.length > 0) {\n videoCodec = video.videoTracks[0].label || \"\"; // This is not always reliable\n }\n if (video.audioTracks && video.audioTracks.length > 0) {\n audioCodec = video.audioTracks[0].label || \"\"; // This is not always reliable\n }\n\n const hasAudio =\n Boolean(video.audioTracks && video.audioTracks.length) ||\n video.mozHasAudio ||\n Boolean(video.webkitAudioDecodedByteCount);\n\n let fileSize = 0;\n try {\n fileSize = await this.getFileSize(sourceFilename, options);\n } catch (error) {\n console.error(\"Error fetching file size:\", error);\n }\n\n // just an estimate by formula Bitrate (Bps) = (File Size (in kilobytes) / Duration (in seconds)) * 8\n const bitRate =\n fileSize && duration ? Math.floor(fileSize / duration) : 0;\n\n video.remove();\n\n let audioChannels = 0;\n let audioSampleRate = 0;\n\n try {\n const audioBuffer = await this.getAudioBuffer(sourceFilename);\n audioChannels = audioBuffer.numberOfChannels;\n audioSampleRate = audioBuffer.sampleRate;\n } catch (error) {\n console.error(\"Error fetching audio buffer:\", error);\n }\n\n let fps = 0;\n try {\n fps = this.getVideoFrameRate(video);\n } catch {\n // Ignore\n }\n\n resolve({\n duration,\n width,\n height,\n bitRate,\n fileSize,\n hasAudio,\n audioSampleRate, // Not available\n audioChannels, // Not available\n audioCodec,\n codec: videoCodec,\n fps, // Not available\n orientation: width >= height ? \"Landscape\" : \"Portrait\",\n });\n };\n\n video.onerror = () => {\n reject(new Error(\"Failed to load video metadata\"));\n };\n\n video.src = sourceFilename; // Set the source of the video\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} 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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-video-metadata",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"dependencies": {},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/react": "^18.2.45",
|
|
39
|
-
"expo-module-scripts": "^3.
|
|
40
|
-
"expo-modules-core": "^1.
|
|
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,22 +22,27 @@ 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"
|
|
@@ -3,63 +3,57 @@ import type {
|
|
|
3
3
|
VideoInfoResult,
|
|
4
4
|
} from "./ExpoVideoMetadata.types";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
interface AudioTrack {
|
|
6
|
+
interface Track {
|
|
8
7
|
id: string;
|
|
9
8
|
kind: string;
|
|
10
9
|
label: string;
|
|
11
10
|
language: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface AudioTrack extends Track {
|
|
12
14
|
enabled: boolean;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
interface VideoTrack {
|
|
16
|
-
id: string;
|
|
17
|
-
kind: string;
|
|
18
|
-
label: string;
|
|
19
|
-
language: string;
|
|
17
|
+
interface VideoTrack extends Track {
|
|
20
18
|
selected: boolean;
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
type AudioTrackList = AudioTrack[];
|
|
25
|
-
type VideoTrackList = VideoTrack[];
|
|
21
|
+
type TrackList<T> = T[];
|
|
26
22
|
|
|
27
|
-
// Extend the HTMLVideoElement type for browsers that support videoTracks and audioTracks
|
|
28
23
|
interface HTMLVideoElementWithTracks extends HTMLVideoElement {
|
|
29
|
-
videoTracks?:
|
|
30
|
-
audioTracks?:
|
|
24
|
+
videoTracks?: TrackList<VideoTrack>;
|
|
25
|
+
audioTracks?: TrackList<AudioTrack>;
|
|
31
26
|
mozHasAudio?: boolean;
|
|
32
27
|
webkitAudioDecodedByteCount?: number;
|
|
28
|
+
captureStream?(): MediaStream;
|
|
33
29
|
}
|
|
34
30
|
|
|
35
31
|
export default {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
getVideoFrameRate(videoElement) {
|
|
32
|
+
name: "ExpoVideoMetadata",
|
|
33
|
+
|
|
34
|
+
getVideoFrameRate(videoElement: HTMLVideoElementWithTracks) {
|
|
40
35
|
if (!videoElement.captureStream) {
|
|
41
|
-
console.
|
|
36
|
+
console.info("captureStream method not supported");
|
|
42
37
|
return 0;
|
|
43
38
|
}
|
|
44
39
|
|
|
45
40
|
const stream = videoElement.captureStream();
|
|
46
|
-
const videoTrack = stream.getVideoTracks()
|
|
41
|
+
const [videoTrack] = stream.getVideoTracks();
|
|
47
42
|
|
|
48
43
|
if (!videoTrack) {
|
|
49
|
-
console.
|
|
44
|
+
console.info("No video track found");
|
|
50
45
|
return 0;
|
|
51
46
|
}
|
|
52
47
|
|
|
53
|
-
|
|
54
|
-
return settings.frameRate;
|
|
48
|
+
return videoTrack.getSettings().frameRate ?? 0;
|
|
55
49
|
},
|
|
50
|
+
|
|
56
51
|
async getAudioBuffer(
|
|
57
52
|
audioUrl: string,
|
|
58
|
-
options = {}
|
|
53
|
+
options: RequestInit = {}
|
|
59
54
|
): Promise<{ sampleRate: number; numberOfChannels: number }> {
|
|
60
55
|
const audioContext = new (window.AudioContext ||
|
|
61
|
-
|
|
62
|
-
window.webkitAudioContext)();
|
|
56
|
+
(window as any).webkitAudioContext)();
|
|
63
57
|
|
|
64
58
|
try {
|
|
65
59
|
const response = await fetch(audioUrl, options);
|
|
@@ -68,144 +62,123 @@ export default {
|
|
|
68
62
|
return new Promise((resolve, reject) => {
|
|
69
63
|
audioContext.decodeAudioData(
|
|
70
64
|
arrayBuffer,
|
|
71
|
-
(
|
|
72
|
-
resolve({
|
|
73
|
-
sampleRate: audioBuffer.sampleRate,
|
|
74
|
-
numberOfChannels: audioBuffer.numberOfChannels,
|
|
75
|
-
});
|
|
76
|
-
},
|
|
65
|
+
({ sampleRate, numberOfChannels }) =>
|
|
66
|
+
resolve({ sampleRate, numberOfChannels }),
|
|
77
67
|
reject
|
|
78
68
|
);
|
|
79
69
|
});
|
|
80
70
|
} catch (error) {
|
|
81
|
-
// Handle fetch errors or other errors here
|
|
82
71
|
throw new Error(
|
|
83
|
-
|
|
72
|
+
`Error fetching or decoding audio file: ${error.message}`
|
|
84
73
|
);
|
|
85
74
|
} finally {
|
|
86
|
-
audioContext.close();
|
|
75
|
+
await audioContext.close();
|
|
87
76
|
}
|
|
88
77
|
},
|
|
89
|
-
getBase64FileSize(base64String: string) {
|
|
90
|
-
// Use a regular expression to remove any data URL prefix
|
|
91
|
-
const base64Data = base64String.replace(/^data:.+;base64,/, "");
|
|
92
|
-
|
|
93
|
-
// Decode the base64 string
|
|
94
|
-
const binaryString = atob(base64Data);
|
|
95
|
-
|
|
96
|
-
// Create an ArrayBuffer with the same length as the binary string
|
|
97
|
-
const bytes = new Uint8Array(binaryString.length);
|
|
98
|
-
|
|
99
|
-
// Fill the ArrayBuffer with elements from the binary string
|
|
100
|
-
for (let i = 0; i < binaryString.length; i++) {
|
|
101
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
102
|
-
}
|
|
103
78
|
|
|
104
|
-
|
|
105
|
-
|
|
79
|
+
getBase64FileSize(base64String: string): number {
|
|
80
|
+
const base64Data = base64String.replace(/^data:.+;base64,/, "");
|
|
81
|
+
return atob(base64Data).length;
|
|
106
82
|
},
|
|
107
|
-
|
|
83
|
+
|
|
84
|
+
async getFileSize(url: string, options: RequestInit = {}): Promise<number> {
|
|
108
85
|
if (url.startsWith("data:")) {
|
|
109
|
-
// Handle data URL
|
|
110
86
|
return this.getBase64FileSize(url);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
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;
|
|
121
96
|
}
|
|
122
97
|
},
|
|
98
|
+
|
|
123
99
|
async getVideoInfo(
|
|
124
|
-
sourceFilename: string,
|
|
100
|
+
sourceFilename: string | File | Blob,
|
|
125
101
|
options: VideoInfoOptions = {}
|
|
126
102
|
): Promise<VideoInfoResult> {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const duration = video.duration; // in seconds
|
|
139
|
-
const width = video.videoWidth;
|
|
140
|
-
const height = video.videoHeight;
|
|
141
|
-
|
|
142
|
-
// Attempt to extract codec information
|
|
143
|
-
let videoCodec = "";
|
|
144
|
-
let audioCodec = "";
|
|
145
|
-
if (video.videoTracks && video.videoTracks.length > 0) {
|
|
146
|
-
videoCodec = video.videoTracks[0].label || ""; // This is not always reliable
|
|
147
|
-
}
|
|
148
|
-
if (video.audioTracks && video.audioTracks.length > 0) {
|
|
149
|
-
audioCodec = video.audioTracks[0].label || ""; // This is not always reliable
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const hasAudio =
|
|
153
|
-
Boolean(video.audioTracks && video.audioTracks.length) ||
|
|
154
|
-
video.mozHasAudio ||
|
|
155
|
-
Boolean(video.webkitAudioDecodedByteCount);
|
|
156
|
-
|
|
157
|
-
let fileSize = 0;
|
|
158
|
-
try {
|
|
159
|
-
fileSize = await this.getFileSize(sourceFilename, options);
|
|
160
|
-
} catch (error) {
|
|
161
|
-
console.error("Error fetching file size:", error);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// just an estimate by formula Bitrate (Bps) = (File Size (in kilobytes) / Duration (in seconds)) * 8
|
|
165
|
-
const bitRate =
|
|
166
|
-
fileSize && duration ? Math.floor(fileSize / duration) : 0;
|
|
167
|
-
|
|
168
|
-
video.remove();
|
|
169
|
-
|
|
170
|
-
let audioChannels = 0;
|
|
171
|
-
let audioSampleRate = 0;
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
const audioBuffer = await this.getAudioBuffer(sourceFilename);
|
|
175
|
-
audioChannels = audioBuffer.numberOfChannels;
|
|
176
|
-
audioSampleRate = audioBuffer.sampleRate;
|
|
177
|
-
} catch (error) {
|
|
178
|
-
console.error("Error fetching audio buffer:", error);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
let fps = 0;
|
|
182
|
-
try {
|
|
183
|
-
fps = this.getVideoFrameRate(video);
|
|
184
|
-
} catch {
|
|
185
|
-
// Ignore
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
resolve({
|
|
189
|
-
duration,
|
|
190
|
-
width,
|
|
191
|
-
height,
|
|
192
|
-
bitRate,
|
|
193
|
-
fileSize,
|
|
194
|
-
hasAudio,
|
|
195
|
-
audioSampleRate, // Not available
|
|
196
|
-
audioChannels, // Not available
|
|
197
|
-
audioCodec,
|
|
198
|
-
codec: videoCodec,
|
|
199
|
-
fps, // Not available
|
|
200
|
-
orientation: width >= height ? "Landscape" : "Portrait",
|
|
201
|
-
});
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
video.onerror = () => {
|
|
205
|
-
reject(new Error("Failed to load video metadata"));
|
|
206
|
-
};
|
|
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
|
+
}
|
|
207
114
|
|
|
208
|
-
|
|
115
|
+
Object.assign(video, {
|
|
116
|
+
crossOrigin: "anonymous",
|
|
117
|
+
autoplay: true,
|
|
118
|
+
muted: true,
|
|
119
|
+
playsInline: true,
|
|
209
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
|
+
}
|
|
210
183
|
},
|
|
211
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);
|