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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # expo-video-metadata
2
2
 
3
- Provides a function that let you get some metadata from video files, like the duration, width, height, fps, codec, hasAudio, orientation, audioChannels, audioCodec, audioSampleRate etc. Check the exported types for more information. This library has web support but is limited to platform APIs and might not work on all browsers correctly.
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;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,WAAW,EACP,UAAU,GACV,oBAAoB,GACpB,WAAW,GACX,gBAAgB,GAChB,eAAe,CAAC;IACpB;;OAEG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,CAAC"}
1
+ {"version":3,"file":"ExpoVideoMetadata.types.d.ts","sourceRoot":"","sources":["../src/ExpoVideoMetadata.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG;IAC5B;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IACtB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,WAAW,EACP,UAAU,GACV,oBAAoB,GACpB,WAAW,GACX,gBAAgB,GAChB,eAAe,CAAC;IACpB;;OAEG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;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
- readonly name: string;
4
- getVideoFrameRate(videoElement: any): any;
5
- getAudioBuffer(audioUrl: string, options?: {}): Promise<{
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?: {}): Promise<number>;
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;;;;6BAqDrB,MAAM,iBAEf,QAAQ;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,CAAC;oCA8B5B,MAAM;qBAkBf,MAAM;iCAiBX,MAAM,YACb,gBAAgB,GACxB,QAAQ,eAAe,CAAC;;AA3F7B,wBAgLE"}
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
- get name() {
3
- return "ExpoVideoMetadata";
4
- },
2
+ name: "ExpoVideoMetadata",
5
3
  getVideoFrameRate(videoElement) {
6
4
  if (!videoElement.captureStream) {
7
- console.error("captureStream method not supported");
5
+ console.info("captureStream method not supported");
8
6
  return 0;
9
7
  }
10
8
  const stream = videoElement.captureStream();
11
- const videoTrack = stream.getVideoTracks()[0];
9
+ const [videoTrack] = stream.getVideoTracks();
12
10
  if (!videoTrack) {
13
- console.error("No video track found");
11
+ console.info("No video track found");
14
12
  return 0;
15
13
  }
16
- const settings = videoTrack.getSettings();
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, (audioBuffer) => {
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
- // Handle fetch errors or other errors here
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
- // Decode the base64 string
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
- else {
63
- // Handle regular URL
64
- try {
65
- const response = await fetch(url, { method: "HEAD", ...options });
66
- const contentLength = response.headers.get("Content-Length");
67
- return contentLength ? parseInt(contentLength, 10) : 0;
68
- }
69
- catch (error) {
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
- return new Promise((resolve, reject) => {
77
- const video = document.createElement("video");
78
- // this is required for Chrome, otherwise it can't detect audio
79
- video.muted = true;
80
- video.autoplay = true;
81
- // we need to wait for `loadeddata` instead of `loadedmetadata` because audioTracks and videoTracks are not available until then
82
- video.onloadeddata = async () => {
83
- const duration = video.duration; // in seconds
84
- const width = video.videoWidth;
85
- const height = video.videoHeight;
86
- // Attempt to extract codec information
87
- let videoCodec = "";
88
- let audioCodec = "";
89
- if (video.videoTracks && video.videoTracks.length > 0) {
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":"AA8BA,eAAe;IACb,IAAI,EAAE,mBAAmB;IAEzB,iBAAiB,CAAC,YAAwC;QACxD,IAAI,CAAC,YAAY,CAAC,aAAa,EAAE,CAAC;YAChC,OAAO,CAAC,IAAI,CAAC,oCAAoC,CAAC,CAAC;YACnD,OAAO,CAAC,CAAC;QACX,CAAC;QAED,MAAM,MAAM,GAAG,YAAY,CAAC,aAAa,EAAE,CAAC;QAC5C,MAAM,CAAC,UAAU,CAAC,GAAG,MAAM,CAAC,cAAc,EAAE,CAAC;QAE7C,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,OAAO,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;YACrC,OAAO,CAAC,CAAC;QACX,CAAC;QAED,OAAO,UAAU,CAAC,WAAW,EAAE,CAAC,SAAS,IAAI,CAAC,CAAC;IACjD,CAAC;IAED,KAAK,CAAC,cAAc,CAClB,QAAgB,EAChB,UAAuB,EAAE;QAEzB,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY;YAC1C,MAAc,CAAC,kBAAkB,CAAC,EAAE,CAAC;QAExC,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YAChD,MAAM,WAAW,GAAG,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC;YAEjD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBACrC,YAAY,CAAC,eAAe,CAC1B,WAAW,EACX,CAAC,EAAE,UAAU,EAAE,gBAAgB,EAAE,EAAE,EAAE,CACnC,OAAO,CAAC,EAAE,UAAU,EAAE,gBAAgB,EAAE,CAAC,EAC3C,MAAM,CACP,CAAC;YACJ,CAAC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACb,0CAA0C,KAAK,CAAC,OAAO,EAAE,CAC1D,CAAC;QACJ,CAAC;gBAAS,CAAC;YACT,MAAM,YAAY,CAAC,KAAK,EAAE,CAAC;QAC7B,CAAC;IACH,CAAC;IAED,iBAAiB,CAAC,YAAoB;QACpC,MAAM,UAAU,GAAG,YAAY,CAAC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;QAChE,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC;IACjC,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,GAAW,EAAE,UAAuB,EAAE;QACtD,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5B,OAAO,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;QACrC,CAAC;QAED,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;YAClE,MAAM,aAAa,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;YAC7D,OAAO,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACzD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,mCAAmC,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;YAC/D,OAAO,CAAC,CAAC;QACX,CAAC;IACH,CAAC;IAED,KAAK,CAAC,YAAY,CAChB,cAAoC,EACpC,UAA4B,EAAE;QAE9B,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAA+B,CAAC;QAC5E,IAAI,QAAQ,GAAG,EAAE,CAAC;QAElB,IAAI,OAAO,cAAc,KAAK,QAAQ,EAAE,CAAC;YACvC,QAAQ,GAAG,cAAc,CAAC;QAC5B,CAAC;aAAM,IACL,cAAc,YAAY,IAAI;YAC9B,cAAc,YAAY,IAAI,EAC9B,CAAC;YACD,QAAQ,GAAG,GAAG,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC;QACjD,CAAC;QAED,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE;YACnB,WAAW,EAAE,WAAW;YACxB,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,IAAI;YACX,WAAW,EAAE,IAAI;SAClB,CAAC,CAAC;QAEH,MAAM,UAAU,GAAG,GAAG,EAAE;YACtB,KAAK,CAAC,KAAK,EAAE,CAAC;YACd,KAAK,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;YAC7B,KAAK,CAAC,IAAI,EAAE,CAAC;YACb,KAAK,CAAC,MAAM,EAAE,CAAC;QACjB,CAAC,CAAC;QAEF,IAAI,CAAC;YACH,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBAC1C,2GAA2G;gBAC3G,KAAK,CAAC,YAAY,GAAG,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;gBACrC,KAAK,CAAC,OAAO,GAAG,GAAG,EAAE,CACnB,MAAM,CAAC,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC,CAAC;gBACrD,KAAK,CAAC,GAAG,GAAG,QAAQ,CAAC;gBACrB,KAAK,CAAC,IAAI,EAAE,CAAC;gBACb,KAAK,CAAC,KAAK,EAAE,CAAC;YAChB,CAAC,CAAC,CAAC;YAEH,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;YACnE,MAAM,UAAU,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC;YAC1C,MAAM,UAAU,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC;YAE1C,MAAM,QAAQ,GACZ,OAAO,CAAC,KAAK,CAAC,WAAW,EAAE,MAAM,CAAC;gBAClC,KAAK,CAAC,WAAW;gBACjB,OAAO,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;YAE7C,MAAM,QAAQ,GACZ,cAAc,YAAY,IAAI,IAAI,cAAc,YAAY,IAAI;gBAC9D,CAAC,CAAC,cAAc,CAAC,IAAI;gBACrB,CAAC,CAAC,MAAM,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YAEhD,MAAM,OAAO,GACX,QAAQ,IAAI,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAE7D,MAAM,EAAE,gBAAgB,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,GACpE,MAAM,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;YAEtC,MAAM,GAAG,GAAG,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;YAE1C,OAAO;gBACL,QAAQ;gBACR,KAAK;gBACL,MAAM;gBACN,OAAO;gBACP,QAAQ;gBACR,QAAQ;gBACR,eAAe;gBACf,KAAK,EAAE,IAAI;gBACX,UAAU,EAAE,UAAU,EAAE,KAAK,IAAI,EAAE;gBACnC,KAAK,EAAE,UAAU,EAAE,KAAK,IAAI,EAAE;gBAC9B,aAAa;gBACb,GAAG;gBACH,WAAW,EAAE,KAAK,IAAI,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU;aACxD,CAAC;QACJ,CAAC;gBAAS,CAAC;YACT,UAAU,EAAE,CAAC;YACb,IAAI,cAAc,YAAY,IAAI,IAAI,cAAc,YAAY,IAAI,EAAE,CAAC;gBACrE,GAAG,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;YAChC,CAAC;QACH,CAAC;IACH,CAAC;CACF,CAAC","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
@@ -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,EACtB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAE1B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAG9E,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,CAAC;AAK7C;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,EACpC,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAE1B"}
@@ -1 +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,cAAsB,EACtB,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,\n options: VideoInfoOptions = {}\n): Promise<VideoInfoResult> {\n return await ExpoVideoMetadataModule.getVideoInfo(sourceFilename, options);\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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.1.0",
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.1.0",
40
- "expo-modules-core": "^1.5.12"
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
- // Define minimal structures for AudioTrack and VideoTrack based on the specification
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
- // Define AudioTrackList and VideoTrackList as arrays of their respective track types
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?: VideoTrackList;
30
- audioTracks?: AudioTrackList;
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
- get name(): string {
37
- return "ExpoVideoMetadata";
38
- },
39
- getVideoFrameRate(videoElement) {
32
+ name: "ExpoVideoMetadata",
33
+
34
+ getVideoFrameRate(videoElement: HTMLVideoElementWithTracks) {
40
35
  if (!videoElement.captureStream) {
41
- console.error("captureStream method not supported");
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()[0];
41
+ const [videoTrack] = stream.getVideoTracks();
47
42
 
48
43
  if (!videoTrack) {
49
- console.error("No video track found");
44
+ console.info("No video track found");
50
45
  return 0;
51
46
  }
52
47
 
53
- const settings = videoTrack.getSettings();
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
- // @ts-expect-error
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
- (audioBuffer) => {
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
- "Error fetching or decoding audio file: " + error.message
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
- // The length of the ArrayBuffer is the size in bytes
105
- return bytes.length;
79
+ getBase64FileSize(base64String: string): number {
80
+ const base64Data = base64String.replace(/^data:.+;base64,/, "");
81
+ return atob(base64Data).length;
106
82
  },
107
- async getFileSize(url: string, options = {}) {
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
- } else {
112
- // Handle regular URL
113
- try {
114
- const response = await fetch(url, { method: "HEAD", ...options });
115
- const contentLength = response.headers.get("Content-Length");
116
- return contentLength ? parseInt(contentLength, 10) : 0;
117
- } catch (error) {
118
- console.error("Error fetching file size for URL:", url, error);
119
- return 0;
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
- return new Promise((resolve, reject) => {
128
- const video = document.createElement(
129
- "video"
130
- ) as HTMLVideoElementWithTracks;
131
-
132
- // this is required for Chrome, otherwise it can't detect audio
133
- video.muted = true;
134
- video.autoplay = true;
135
-
136
- // we need to wait for `loadeddata` instead of `loadedmetadata` because audioTracks and videoTracks are not available until then
137
- video.onloadeddata = async () => {
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
- video.src = sourceFilename; // Set the source of the video
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);
package/tea.yaml ADDED
@@ -0,0 +1,6 @@
1
+ # https://tea.xyz/what-is-this-file
2
+ ---
3
+ version: 1.0.0
4
+ codeOwners:
5
+ - '0x92c6F5D59bf78D24bE942794ec9F08e887a9E7e0'
6
+ quorum: 1