expo-video-metadata 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc.js CHANGED
@@ -1,5 +1,8 @@
1
1
  module.exports = {
2
2
  root: true,
3
- extends: ['universe/native', 'universe/web'],
4
- ignorePatterns: ['build'],
3
+ extends: ["universe/native", "universe/web"],
4
+ ignorePatterns: ["build"],
5
+ rules: {
6
+ "comma-dangle": 0,
7
+ },
5
8
  };
@@ -0,0 +1,3 @@
1
+ {
2
+ "trailingComma": "es5"
3
+ }
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. Web support is not available yet but it is planned.
3
+ This React Native (Expo) library provides a versatile function designed to extract a wide range of metadata from video files, including duration, width, height, frame rate, codec, audio availability, orientation, audio channels, audio codec, and audio sample rate. For comprehensive details, please refer to the listed exported types. Although the library is equipped with web support, its performance is reliant on specific platform APIs, leading to potential variability in its effectiveness across different browsers.
4
4
 
5
5
  <img src="https://raw.githubusercontent.com/hirbod/expo-video-metadata/assets/preview.png" width="500" />
6
6
 
@@ -38,12 +38,7 @@ import { getVideoInfoAsync } from 'expo-video-metadata';
38
38
  * @return Returns a promise which fulfils with [`VideoInfoResult`](#Videoinforesult).
39
39
  */
40
40
 
41
- const result = await getVideoInfoAsync(
42
- sourceFilename: string,
43
- options: VideoInfoOptions = {},
44
- ): Promise<VideoInfoResult> {
45
- return await ExpoVideoMetadataModule.getVideoInfo(sourceFilename, options);
46
- }
41
+ const result = await getVideoInfoAsync(sourceFilename: string, options: VideoInfoOptions = {}): Promise<VideoInfoResult>
47
42
  ```
48
43
 
49
44
  See [VideoInfoResult](https://github.com/hirbod/expo-video-metadata/blob/b9239224eed46f455b2fb9f1b29e69ac49da6683/src/ExpoVideoMetadata.types.ts#L1) type for more information.
@@ -17,6 +17,7 @@ import kotlinx.coroutines.cancel
17
17
  import kotlinx.coroutines.launch
18
18
  import java.io.File
19
19
  import android.media.MediaExtractor
20
+ import android.media.MediaFeature
20
21
  import android.media.MediaFormat
21
22
  import java.math.BigDecimal
22
23
  import java.math.RoundingMode
@@ -69,6 +70,12 @@ class ExpoVideoMetadataModule : Module() {
69
70
  val rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toIntOrNull()
70
71
  val hasAudio = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO) != null
71
72
 
73
+ val colorTransfer = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COLOR_TRANSFER)?.toIntOrNull()
74
+ var isHDR: Boolean? = null
75
+ if (colorTransfer != null) {
76
+ isHDR = colorTransfer == MediaFormat.COLOR_TRANSFER_ST2084 || colorTransfer == MediaFormat.COLOR_TRANSFER_HLG
77
+ }
78
+
72
79
  // release
73
80
  retriever.release()
74
81
 
@@ -115,6 +122,7 @@ class ExpoVideoMetadataModule : Module() {
115
122
  "bitrate" to bitrate,
116
123
  "fileSize" to fileSize,
117
124
  "hasAudio" to hasAudio,
125
+ "isHDR" to isHDR,
118
126
  "audioCodec" to audioCodec,
119
127
  "orientation" to getOrientation(rotation),
120
128
  "audioSampleRate" to audioSampleRate,
@@ -7,6 +7,11 @@ export type VideoInfoResult = {
7
7
  * Tells if the video has a audio track. If the video has no audio track, its considered a mute video.
8
8
  */
9
9
  hasAudio: boolean;
10
+ /**
11
+ * Available only on iOS >= 14 and Android. Tells if the video is a HDR video.
12
+ * Will return null if it could not be determined. (e.g. on Web or on older iOS/Android versions)
13
+ */
14
+ isHDR: boolean | null;
10
15
  /**
11
16
  * Width of the video in pixels.
12
17
  */
@@ -17,24 +22,29 @@ export type VideoInfoResult = {
17
22
  height: number;
18
23
  /**
19
24
  * Frame rate of the video in frames per second.
25
+ * Works on iOS, Android and Web (except Safari).
20
26
  */
21
27
  fps: number;
22
28
  /**
23
29
  * Bit rate of the video in bits per second.
30
+ * Supported on all platforms.
24
31
  */
25
32
  bitRate: number;
26
33
  /**
27
34
  * File size of the video in bytes. Works only for local files, returns 0 for remote files.
35
+ * Supported on all platforms.
28
36
  */
29
37
  fileSize: number;
30
38
  /**
31
39
  * Video codec.
40
+ * Supported on all platforms, but on Web it may return an empty string.
32
41
  */
33
42
  codec: string;
34
43
  /**
35
44
  * Video orientation.
45
+ * Supported on all platforms, but on Web it may return an empty string.
36
46
  */
37
- orientation: "Portrait" | "PortraitUpsideDown" | "LandscapeRight" | "LandscapeLeft";
47
+ orientation: "Portrait" | "PortraitUpsideDown" | "Landscape" | "LandscapeRight" | "LandscapeLeft";
38
48
  /**
39
49
  * Audio sample rate of the video in samples per second.
40
50
  */
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoVideoMetadata.types.d.ts","sourceRoot":"","sources":["../src/ExpoVideoMetadata.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG;IAC5B;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;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,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 | \"LandscapeRight\"\n | \"LandscapeLeft\";\n /**\n * Audio sample rate of the video in samples per second.\n */\n audioSampleRate: number;\n /**\n * Audio channel count of the video.\n */\n audioChannels: number;\n /**\n * Audio codec of the video.\n */\n audioCodec: string;\n};\n\nexport type VideoInfoOptions = {\n /**\n * In case `sourceFilename` is a remote URI, `headers` object is passed in a network request.\n */\n headers?: Record<string, string>;\n};\n"]}
1
+ {"version":3,"file":"ExpoVideoMetadata.types.js","sourceRoot":"","sources":["../src/ExpoVideoMetadata.types.ts"],"names":[],"mappings":"","sourcesContent":["export type VideoInfoResult = {\n /**\n * Duration of the video in seconds (float).\n */\n duration: number;\n /**\n * Tells if the video has a audio track. If the video has no audio track, its considered a mute video.\n */\n hasAudio: boolean;\n /**\n * Available only on iOS >= 14 and Android. Tells if the video is a HDR video.\n * Will return null if it could not be determined. (e.g. on Web or on older iOS/Android versions)\n */\n isHDR: boolean | null;\n /**\n * Width of the video in pixels.\n */\n width: number;\n /**\n * Height of the video in pixels.\n */\n height: number;\n /**\n * Frame rate of the video in frames per second.\n * Works on iOS, Android and Web (except Safari).\n */\n fps: number;\n /**\n * Bit rate of the video in bits per second.\n * Supported on all platforms.\n */\n bitRate: number;\n /**\n * File size of the video in bytes. Works only for local files, returns 0 for remote files.\n * Supported on all platforms.\n */\n fileSize: number;\n /**\n * Video codec.\n * Supported on all platforms, but on Web it may return an empty string.\n */\n codec: string;\n /**\n * Video orientation.\n * Supported on all platforms, but on Web it may return an empty string.\n */\n orientation:\n | \"Portrait\"\n | \"PortraitUpsideDown\"\n | \"Landscape\"\n | \"LandscapeRight\"\n | \"LandscapeLeft\";\n /**\n * Audio sample rate of the video in samples per second.\n */\n audioSampleRate: number;\n /**\n * Audio channel count of the video.\n */\n audioChannels: number;\n /**\n * Audio codec of the video.\n */\n audioCodec: string;\n};\n\nexport type VideoInfoOptions = {\n /**\n * In case `sourceFilename` is a remote URI, `headers` object is passed in a network request.\n */\n headers?: Record<string, string>;\n};\n"]}
@@ -1,7 +1,34 @@
1
1
  import type { VideoInfoOptions, VideoInfoResult } from "./ExpoVideoMetadata.types";
2
+ interface Track {
3
+ id: string;
4
+ kind: string;
5
+ label: string;
6
+ language: string;
7
+ }
8
+ interface AudioTrack extends Track {
9
+ enabled: boolean;
10
+ }
11
+ interface VideoTrack extends Track {
12
+ selected: boolean;
13
+ }
14
+ type TrackList<T> = T[];
15
+ interface HTMLVideoElementWithTracks extends HTMLVideoElement {
16
+ videoTracks?: TrackList<VideoTrack>;
17
+ audioTracks?: TrackList<AudioTrack>;
18
+ mozHasAudio?: boolean;
19
+ webkitAudioDecodedByteCount?: number;
20
+ captureStream?(): MediaStream;
21
+ }
2
22
  declare const _default: {
3
- readonly name: string;
4
- getVideoInfoAsync(sourceFilename: string, options?: VideoInfoOptions): Promise<VideoInfoResult>;
23
+ name: string;
24
+ getVideoFrameRate(videoElement: HTMLVideoElementWithTracks): number;
25
+ getAudioBuffer(audioUrl: string, options?: RequestInit): Promise<{
26
+ sampleRate: number;
27
+ numberOfChannels: number;
28
+ }>;
29
+ getBase64FileSize(base64String: string): number;
30
+ getFileSize(url: string, options?: RequestInit): Promise<number>;
31
+ getVideoInfo(sourceFilename: string | File | Blob, options?: VideoInfoOptions): Promise<VideoInfoResult>;
5
32
  };
6
33
  export default _default;
7
34
  //# sourceMappingURL=ExpoVideoMetadataModule.web.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoVideoMetadataModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoVideoMetadataModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EAChB,MAAM,2BAA2B,CAAC;;;sCAOf,MAAM,YACb,gBAAgB,GACxB,QAAQ,eAAe,CAAC;;AAP7B,wBAUE"}
1
+ {"version":3,"file":"ExpoVideoMetadataModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoVideoMetadataModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EAChB,MAAM,2BAA2B,CAAC;AAEnC,UAAU,KAAK;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,UAAW,SAAQ,KAAK;IAChC,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,UAAU,UAAW,SAAQ,KAAK;IAChC,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;AAExB,UAAU,0BAA2B,SAAQ,gBAAgB;IAC3D,WAAW,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,aAAa,CAAC,IAAI,WAAW,CAAC;CAC/B;;;oCAKiC,0BAA0B;6BAkB9C,MAAM,YACP,WAAW,GACnB,QAAQ;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,CAAC;oCAyB5B,MAAM,GAAG,MAAM;qBAKxB,MAAM,YAAW,WAAW,GAAQ,QAAQ,MAAM,CAAC;iCAgBxD,MAAM,GAAG,IAAI,GAAG,IAAI,YAC3B,gBAAgB,GACxB,QAAQ,eAAe,CAAC;;AAvE7B,wBAyJE"}
@@ -1,9 +1,118 @@
1
1
  export default {
2
- get name() {
3
- return "ExpoVideoMetadata";
2
+ name: "ExpoVideoMetadata",
3
+ getVideoFrameRate(videoElement) {
4
+ if (!videoElement.captureStream) {
5
+ console.info("captureStream method not supported");
6
+ return 0;
7
+ }
8
+ const stream = videoElement.captureStream();
9
+ const [videoTrack] = stream.getVideoTracks();
10
+ if (!videoTrack) {
11
+ console.info("No video track found");
12
+ return 0;
13
+ }
14
+ return videoTrack.getSettings().frameRate ?? 0;
4
15
  },
5
- async getVideoInfoAsync(sourceFilename, options = {}) {
6
- throw new Error("ExpoVideoMetadata not supported on Expo Web yet");
16
+ async getAudioBuffer(audioUrl, options = {}) {
17
+ const audioContext = new (window.AudioContext ||
18
+ window.webkitAudioContext)();
19
+ try {
20
+ const response = await fetch(audioUrl, options);
21
+ const arrayBuffer = await response.arrayBuffer();
22
+ return new Promise((resolve, reject) => {
23
+ audioContext.decodeAudioData(arrayBuffer, ({ sampleRate, numberOfChannels }) => resolve({ sampleRate, numberOfChannels }), reject);
24
+ });
25
+ }
26
+ catch (error) {
27
+ throw new Error(`Error fetching or decoding audio file: ${error.message}`);
28
+ }
29
+ finally {
30
+ await audioContext.close();
31
+ }
32
+ },
33
+ getBase64FileSize(base64String) {
34
+ const base64Data = base64String.replace(/^data:.+;base64,/, "");
35
+ return atob(base64Data).length;
36
+ },
37
+ async getFileSize(url, options = {}) {
38
+ if (url.startsWith("data:")) {
39
+ return this.getBase64FileSize(url);
40
+ }
41
+ try {
42
+ const response = await fetch(url, { method: "HEAD", ...options });
43
+ const contentLength = response.headers.get("Content-Length");
44
+ return contentLength ? parseInt(contentLength, 10) : 0;
45
+ }
46
+ catch (error) {
47
+ console.error("Error fetching file size for URL:", url, error);
48
+ return 0;
49
+ }
50
+ },
51
+ async getVideoInfo(sourceFilename, options = {}) {
52
+ const video = document.createElement("video");
53
+ let videoUrl = "";
54
+ if (typeof sourceFilename === "string") {
55
+ videoUrl = sourceFilename;
56
+ }
57
+ else if (sourceFilename instanceof File ||
58
+ sourceFilename instanceof Blob) {
59
+ videoUrl = URL.createObjectURL(sourceFilename);
60
+ }
61
+ Object.assign(video, {
62
+ crossOrigin: "anonymous",
63
+ autoplay: true,
64
+ muted: true,
65
+ playsInline: true,
66
+ });
67
+ const resetVideo = () => {
68
+ video.pause();
69
+ video.removeAttribute("src");
70
+ video.load();
71
+ video.remove();
72
+ };
73
+ try {
74
+ await new Promise((resolve, reject) => {
75
+ // Can't use `loadedmetadata` event because it does not contain videoTracks, audioTracks and other metadata
76
+ video.onloadeddata = () => resolve();
77
+ video.onerror = () => reject(new Error("Failed to load video metadata"));
78
+ video.src = videoUrl;
79
+ video.load();
80
+ video.pause();
81
+ });
82
+ const { duration, videoWidth: width, videoHeight: height } = video;
83
+ const videoTrack = video.videoTracks?.[0];
84
+ const audioTrack = video.audioTracks?.[0];
85
+ const hasAudio = Boolean(video.audioTracks?.length) ||
86
+ video.mozHasAudio ||
87
+ Boolean(video.webkitAudioDecodedByteCount);
88
+ const fileSize = sourceFilename instanceof File || sourceFilename instanceof Blob
89
+ ? sourceFilename.size
90
+ : await this.getFileSize(videoUrl, options);
91
+ const bitRate = fileSize && duration ? Math.floor(fileSize / duration) : 0;
92
+ const { numberOfChannels: audioChannels, sampleRate: audioSampleRate } = await this.getAudioBuffer(videoUrl);
93
+ const fps = this.getVideoFrameRate(video);
94
+ return {
95
+ duration,
96
+ width,
97
+ height,
98
+ bitRate,
99
+ fileSize,
100
+ hasAudio,
101
+ audioSampleRate,
102
+ isHDR: null,
103
+ audioCodec: audioTrack?.label ?? "",
104
+ codec: videoTrack?.label ?? "",
105
+ audioChannels,
106
+ fps,
107
+ orientation: width >= height ? "Landscape" : "Portrait",
108
+ };
109
+ }
110
+ finally {
111
+ resetVideo();
112
+ if (sourceFilename instanceof File || sourceFilename instanceof Blob) {
113
+ URL.revokeObjectURL(videoUrl);
114
+ }
115
+ }
7
116
  },
8
117
  };
9
118
  //# sourceMappingURL=ExpoVideoMetadataModule.web.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoVideoMetadataModule.web.js","sourceRoot":"","sources":["../src/ExpoVideoMetadataModule.web.ts"],"names":[],"mappings":"AAKA,eAAe;IACb,IAAI,IAAI;QACN,OAAO,mBAAmB,CAAC;IAC7B,CAAC;IACD,KAAK,CAAC,iBAAiB,CACrB,cAAsB,EACtB,UAA4B,EAAE;QAE9B,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACrE,CAAC;CACF,CAAC","sourcesContent":["import type {\n VideoInfoOptions,\n VideoInfoResult,\n} from \"./ExpoVideoMetadata.types\";\n\nexport default {\n get name(): string {\n return \"ExpoVideoMetadata\";\n },\n async getVideoInfoAsync(\n sourceFilename: string,\n options: VideoInfoOptions = {},\n ): Promise<VideoInfoResult> {\n throw new Error(\"ExpoVideoMetadata not supported on Expo Web yet\");\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
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "expo-video-metadata",
3
- "version": "1.0.0",
4
- "description": "Module to retrieve metadata from a video with Expo and React Native",
3
+ "version": "1.2.0",
4
+ "description": "Module to retrieve metadata from a video with Web, Expo and React Native",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
7
7
  "scripts": {
@@ -35,9 +35,9 @@
35
35
  "homepage": "https://github.com/hirbod/expo-video-metadata#readme",
36
36
  "dependencies": {},
37
37
  "devDependencies": {
38
- "@types/react": "^18.0.25",
39
- "expo-module-scripts": "^3.0.11",
40
- "expo-modules-core": "^1.5.11"
38
+ "@types/react": "^18.2.45",
39
+ "expo-module-scripts": "^3.5.2",
40
+ "expo-modules-core": "^1.12.18"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "expo": "*",
@@ -7,6 +7,11 @@ export type VideoInfoResult = {
7
7
  * Tells if the video has a audio track. If the video has no audio track, its considered a mute video.
8
8
  */
9
9
  hasAudio: boolean;
10
+ /**
11
+ * Available only on iOS >= 14 and Android. Tells if the video is a HDR video.
12
+ * Will return null if it could not be determined. (e.g. on Web or on older iOS/Android versions)
13
+ */
14
+ isHDR: boolean | null;
10
15
  /**
11
16
  * Width of the video in pixels.
12
17
  */
@@ -17,26 +22,32 @@ export type VideoInfoResult = {
17
22
  height: number;
18
23
  /**
19
24
  * Frame rate of the video in frames per second.
25
+ * Works on iOS, Android and Web (except Safari).
20
26
  */
21
27
  fps: number;
22
28
  /**
23
29
  * Bit rate of the video in bits per second.
30
+ * Supported on all platforms.
24
31
  */
25
32
  bitRate: number;
26
33
  /**
27
34
  * File size of the video in bytes. Works only for local files, returns 0 for remote files.
35
+ * Supported on all platforms.
28
36
  */
29
37
  fileSize: number;
30
38
  /**
31
39
  * Video codec.
40
+ * Supported on all platforms, but on Web it may return an empty string.
32
41
  */
33
42
  codec: string;
34
43
  /**
35
44
  * Video orientation.
45
+ * Supported on all platforms, but on Web it may return an empty string.
36
46
  */
37
47
  orientation:
38
48
  | "Portrait"
39
49
  | "PortraitUpsideDown"
50
+ | "Landscape"
40
51
  | "LandscapeRight"
41
52
  | "LandscapeLeft";
42
53
  /**
@@ -3,14 +3,182 @@ import type {
3
3
  VideoInfoResult,
4
4
  } from "./ExpoVideoMetadata.types";
5
5
 
6
+ interface Track {
7
+ id: string;
8
+ kind: string;
9
+ label: string;
10
+ language: string;
11
+ }
12
+
13
+ interface AudioTrack extends Track {
14
+ enabled: boolean;
15
+ }
16
+
17
+ interface VideoTrack extends Track {
18
+ selected: boolean;
19
+ }
20
+
21
+ type TrackList<T> = T[];
22
+
23
+ interface HTMLVideoElementWithTracks extends HTMLVideoElement {
24
+ videoTracks?: TrackList<VideoTrack>;
25
+ audioTracks?: TrackList<AudioTrack>;
26
+ mozHasAudio?: boolean;
27
+ webkitAudioDecodedByteCount?: number;
28
+ captureStream?(): MediaStream;
29
+ }
30
+
6
31
  export default {
7
- get name(): string {
8
- return "ExpoVideoMetadata";
32
+ name: "ExpoVideoMetadata",
33
+
34
+ getVideoFrameRate(videoElement: HTMLVideoElementWithTracks) {
35
+ if (!videoElement.captureStream) {
36
+ console.info("captureStream method not supported");
37
+ return 0;
38
+ }
39
+
40
+ const stream = videoElement.captureStream();
41
+ const [videoTrack] = stream.getVideoTracks();
42
+
43
+ if (!videoTrack) {
44
+ console.info("No video track found");
45
+ return 0;
46
+ }
47
+
48
+ return videoTrack.getSettings().frameRate ?? 0;
49
+ },
50
+
51
+ async getAudioBuffer(
52
+ audioUrl: string,
53
+ options: RequestInit = {}
54
+ ): Promise<{ sampleRate: number; numberOfChannels: number }> {
55
+ const audioContext = new (window.AudioContext ||
56
+ (window as any).webkitAudioContext)();
57
+
58
+ try {
59
+ const response = await fetch(audioUrl, options);
60
+ const arrayBuffer = await response.arrayBuffer();
61
+
62
+ return new Promise((resolve, reject) => {
63
+ audioContext.decodeAudioData(
64
+ arrayBuffer,
65
+ ({ sampleRate, numberOfChannels }) =>
66
+ resolve({ sampleRate, numberOfChannels }),
67
+ reject
68
+ );
69
+ });
70
+ } catch (error) {
71
+ throw new Error(
72
+ `Error fetching or decoding audio file: ${error.message}`
73
+ );
74
+ } finally {
75
+ await audioContext.close();
76
+ }
77
+ },
78
+
79
+ getBase64FileSize(base64String: string): number {
80
+ const base64Data = base64String.replace(/^data:.+;base64,/, "");
81
+ return atob(base64Data).length;
9
82
  },
10
- async getVideoInfoAsync(
11
- sourceFilename: string,
12
- options: VideoInfoOptions = {},
83
+
84
+ async getFileSize(url: string, options: RequestInit = {}): Promise<number> {
85
+ if (url.startsWith("data:")) {
86
+ return this.getBase64FileSize(url);
87
+ }
88
+
89
+ try {
90
+ const response = await fetch(url, { method: "HEAD", ...options });
91
+ const contentLength = response.headers.get("Content-Length");
92
+ return contentLength ? parseInt(contentLength, 10) : 0;
93
+ } catch (error) {
94
+ console.error("Error fetching file size for URL:", url, error);
95
+ return 0;
96
+ }
97
+ },
98
+
99
+ async getVideoInfo(
100
+ sourceFilename: string | File | Blob,
101
+ options: VideoInfoOptions = {}
13
102
  ): Promise<VideoInfoResult> {
14
- throw new Error("ExpoVideoMetadata not supported on Expo Web yet");
103
+ const video = document.createElement("video") as HTMLVideoElementWithTracks;
104
+ let videoUrl = "";
105
+
106
+ if (typeof sourceFilename === "string") {
107
+ videoUrl = sourceFilename;
108
+ } else if (
109
+ sourceFilename instanceof File ||
110
+ sourceFilename instanceof Blob
111
+ ) {
112
+ videoUrl = URL.createObjectURL(sourceFilename);
113
+ }
114
+
115
+ Object.assign(video, {
116
+ crossOrigin: "anonymous",
117
+ autoplay: true,
118
+ muted: true,
119
+ playsInline: true,
120
+ });
121
+
122
+ const resetVideo = () => {
123
+ video.pause();
124
+ video.removeAttribute("src");
125
+ video.load();
126
+ video.remove();
127
+ };
128
+
129
+ try {
130
+ await new Promise<void>((resolve, reject) => {
131
+ // Can't use `loadedmetadata` event because it does not contain videoTracks, audioTracks and other metadata
132
+ video.onloadeddata = () => resolve();
133
+ video.onerror = () =>
134
+ reject(new Error("Failed to load video metadata"));
135
+ video.src = videoUrl;
136
+ video.load();
137
+ video.pause();
138
+ });
139
+
140
+ const { duration, videoWidth: width, videoHeight: height } = video;
141
+ const videoTrack = video.videoTracks?.[0];
142
+ const audioTrack = video.audioTracks?.[0];
143
+
144
+ const hasAudio =
145
+ Boolean(video.audioTracks?.length) ||
146
+ video.mozHasAudio ||
147
+ Boolean(video.webkitAudioDecodedByteCount);
148
+
149
+ const fileSize =
150
+ sourceFilename instanceof File || sourceFilename instanceof Blob
151
+ ? sourceFilename.size
152
+ : await this.getFileSize(videoUrl, options);
153
+
154
+ const bitRate =
155
+ fileSize && duration ? Math.floor(fileSize / duration) : 0;
156
+
157
+ const { numberOfChannels: audioChannels, sampleRate: audioSampleRate } =
158
+ await this.getAudioBuffer(videoUrl);
159
+
160
+ const fps = this.getVideoFrameRate(video);
161
+
162
+ return {
163
+ duration,
164
+ width,
165
+ height,
166
+ bitRate,
167
+ fileSize,
168
+ hasAudio,
169
+ audioSampleRate,
170
+ isHDR: null,
171
+ audioCodec: audioTrack?.label ?? "",
172
+ codec: videoTrack?.label ?? "",
173
+ audioChannels,
174
+ fps,
175
+ orientation: width >= height ? "Landscape" : "Portrait",
176
+ };
177
+ } finally {
178
+ resetVideo();
179
+ if (sourceFilename instanceof File || sourceFilename instanceof Blob) {
180
+ URL.revokeObjectURL(videoUrl);
181
+ }
182
+ }
15
183
  },
16
184
  };
package/src/index.ts CHANGED
@@ -15,7 +15,7 @@ export { VideoInfoOptions, VideoInfoResult };
15
15
  * @return Returns a promise which fulfils with [`VideoInfoResult`](#Videoinforesult).
16
16
  */
17
17
  export async function getVideoInfoAsync(
18
- sourceFilename: string,
18
+ sourceFilename: string | File | Blob,
19
19
  options: VideoInfoOptions = {}
20
20
  ): Promise<VideoInfoResult> {
21
21
  return await ExpoVideoMetadataModule.getVideoInfo(sourceFilename, options);
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