diff --git a/package.json b/package.json
index 32c17bdb66..086837f0f2 100644
--- a/package.json
+++ b/package.json
@@ -123,6 +123,7 @@
"merkletreejs": "^0.4.0",
"metamask-react": "^2.4.1",
"moment": "^2.29.4",
+ "mp4box": "^0.5.3",
"multiformats": "^12.1.3",
"osmojs": "16.9.0",
"papaparse": "^5.4.1",
diff --git a/packages/components/video/VideoCard.tsx b/packages/components/video/VideoCard.tsx
index 04bac4fdd6..9e8d53177a 100644
--- a/packages/components/video/VideoCard.tsx
+++ b/packages/components/video/VideoCard.tsx
@@ -1,5 +1,7 @@
+import { AVPlaybackStatus } from "expo-av";
import { isEqual } from "lodash";
-import React, { memo, useState } from "react";
+import MP4Box from "mp4box";
+import React, { memo, useEffect, useState } from "react";
import {
StyleProp,
StyleSheet,
@@ -30,7 +32,10 @@ import {
} from "../../utils/style/fonts";
import { layout, RESPONSIVE_BREAKPOINT_S } from "../../utils/style/layout";
import { tinyAddress } from "../../utils/text";
-import { ZodSocialFeedVideoMetadata } from "../../utils/types/feed";
+import {
+ SocialFeedVideoMetadata,
+ ZodSocialFeedVideoMetadata,
+} from "../../utils/types/feed";
import { BrandText } from "../BrandText";
import { OmniLink } from "../OmniLink";
import { OptimizedImage } from "../OptimizedImage";
@@ -40,7 +45,9 @@ import { DateTime } from "../socialFeed/SocialCard/DateTime";
import { SpacerColumn, SpacerRow } from "../spacer";
import { LocationButton } from "@/components/socialFeed/NewsFeed/LocationButton";
+import { useVideoAudioDuration } from "@/hooks/feed/useVideoAudioDuration";
import { useAppNavigation } from "@/hooks/navigation/useAppNavigation";
+import { web3ToWeb2URI } from "@/utils/ipfs";
const IMAGE_HEIGHT = 173;
const VIDEO_CARD_WIDTH = 261;
@@ -56,6 +63,7 @@ export const VideoCard: React.FC<{
const authorNSInfo = useNSUserInfo(post.authorId);
const [, userAddress] = parseUserId(post.authorId);
const [isHovered, setIsHovered] = useState(false);
+ // const [duration, setDuration] = useState(0);
let cardWidth = StyleSheet.flatten(style)?.width;
if (typeof cardWidth !== "number") {
@@ -71,6 +79,18 @@ export const VideoCard: React.FC<{
? video.videoFile.thumbnailFileData.url
: "ipfs://" + video.videoFile.thumbnailFileData?.url // we need this hack because ipfs "urls" in feed are raw CIDs
: defaultThumbnailImage;
+ // useEffect(() => {
+ // (async () => {
+ // try {
+ // if (video && !video.videoFile.videoMetadata?.duration) {
+ // const duration = await getVideoDurationFromURL();
+ // setDuration(duration);
+ // }
+ // } catch (error) {
+ // console.log(error);
+ // }
+ // })();
+ // }, [video]);
if (!video)
return (
@@ -78,6 +98,7 @@ export const VideoCard: React.FC<{
Video not found
);
+
return (
-
-
- {prettyMediaDuration(video.videoFile.videoMetadata?.duration)}
-
-
-
- {video?.location && (
+
+ {video?.location ? (
@@ -135,6 +151,8 @@ export const VideoCard: React.FC<{
stroke={neutralFF}
/>
+ ) : (
+
)}
@@ -208,6 +226,27 @@ export const VideoCard: React.FC<{
);
}, isEqual);
+function VideoDuration({ video }: { video: SocialFeedVideoMetadata }) {
+ const { duration, isLoading } = useVideoAudioDuration(
+ web3ToWeb2URI(video?.videoFile.url),
+ video?.videoFile.videoMetadata?.duration || 0,
+ );
+
+ return (
+ <>
+ {!isLoading ? (
+
+
+ {prettyMediaDuration(duration)}
+
+
+ ) : (
+
+ )}
+ >
+ );
+}
+
const imgBoxStyle: ViewStyle = {
position: "relative",
};
diff --git a/packages/hooks/feed/useVideoAudioDuration.tsx b/packages/hooks/feed/useVideoAudioDuration.tsx
new file mode 100644
index 0000000000..97033bcf72
--- /dev/null
+++ b/packages/hooks/feed/useVideoAudioDuration.tsx
@@ -0,0 +1,24 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { getVideoDurationFromURL } from "@/utils/video";
+
+export const useVideoAudioDuration = (url: string, duration: number) => {
+ const { data, isLoading } = useQuery(
+ ["getVideoDuration", url],
+ async () => {
+ if (duration) return duration;
+
+ try {
+ const duration = await getVideoDurationFromURL(url);
+ console.log(duration);
+ return duration;
+ } catch (error) {
+ console.log(error);
+ return 0;
+ }
+ },
+ { staleTime: Infinity },
+ );
+
+ return { duration: data || 0, isLoading };
+};
diff --git a/packages/utils/types/mp4box.d.ts b/packages/utils/types/mp4box.d.ts
new file mode 100644
index 0000000000..49d1aefcea
--- /dev/null
+++ b/packages/utils/types/mp4box.d.ts
@@ -0,0 +1,43 @@
+declare module "mp4box" {
+ export interface MP4FileInfo {
+ duration: number; // Duration in timescale units
+ timescale: number; // Timescale of the file
+ isFragmented: boolean; // Whether the MP4 is fragmented
+ brands: string[]; // Major and compatible brands
+ created: Date; // Creation time
+ modified: Date; // Modification time
+ tracks: MP4Track[]; // Array of tracks in the file
+ }
+
+ export interface MP4Track {
+ id: number; // Track ID
+ type: string; // Track type (e.g., "video", "audio")
+ codec: string; // Codec used for this track
+ language: string; // Language of the track
+ created: Date; // Creation time
+ modified: Date; // Modification time
+ timescale: number; // Timescale for the track
+ duration: number; // Duration in timescale units
+ bitrate: number; // Average bitrate of the track
+ width?: number; // Video width (if applicable)
+ height?: number; // Video height (if applicable)
+ sampleCount: number; // Number of samples in the track
+ samples: any[]; // Detailed sample information
+ }
+
+ export interface MP4ArrayBuffer extends ArrayBuffer {
+ fileStart: number; // Start position of the buffer in the file
+ }
+
+ export interface MP4BoxFile {
+ onReady?: (info: MP4FileInfo) => void;
+ onError?: (error: string) => void;
+ appendBuffer(data: ArrayBuffer): number;
+ start(): void;
+ stop(): void;
+ flush(): void;
+ createFile(): MP4BoxFile;
+ }
+
+ export function createFile(): MP4BoxFile;
+}
diff --git a/packages/utils/video/index.ts b/packages/utils/video/index.ts
index a31bfdad58..65f69f3b5a 100644
--- a/packages/utils/video/index.ts
+++ b/packages/utils/video/index.ts
@@ -1,3 +1,5 @@
+import MP4Box, { MP4ArrayBuffer, MP4BoxFile, MP4FileInfo } from "mp4box";
+
import { VideoFileMetadata } from "../types/files";
const getVideoDuration = (file: File) => {
@@ -19,11 +21,47 @@ const getVideoDuration = (file: File) => {
} catch (e) {
console.error("Fail to get video duration: ", e);
}
+
+ return duration;
+};
+
+export const getVideoDurationFromBuffer = async (buffer: ArrayBuffer) => {
+ let duration = 0;
+ const mp4boxFile: MP4BoxFile = MP4Box.createFile();
+
+ mp4boxFile.onReady = (info: MP4FileInfo) => {
+ const durationInSec = info.duration / info.timescale;
+ duration = durationInSec * 1000;
+ };
+
+ const fileData = buffer.slice(0);
+ (fileData as MP4ArrayBuffer).fileStart = 0;
+ mp4boxFile.appendBuffer(fileData);
+
+ return duration;
+};
+
+export const getVideoDurationFromURL = async (url: string) => {
+ let duration = 0;
+
+ try {
+ const response = await fetch(url, {
+ method: "GET",
+ });
+ const buffer = await response.arrayBuffer();
+
+ duration = await getVideoDurationFromBuffer(buffer);
+ } catch (error) {
+ console.log(error);
+ }
+
return duration;
};
-export const getVideoData = (file: File): VideoFileMetadata => {
- const duration = getVideoDuration(file) * 1000;
+export const getVideoData = async (file: File): Promise => {
+ const fileBuffer = await file.arrayBuffer();
+ const duration = await getVideoDurationFromBuffer(fileBuffer);
+
return {
duration,
};
diff --git a/yarn.lock b/yarn.lock
index 240fc5ed44..0d67d78ca2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -15881,6 +15881,13 @@ __metadata:
languageName: node
linkType: hard
+"mp4box@npm:^0.5.3":
+ version: 0.5.3
+ resolution: "mp4box@npm:0.5.3"
+ checksum: 6d38b47beebd71d00151f2df6fdc5a3bcafd3163c42783a432a05d9c0dae97120afa14c7a30adeb7480833bde7f75374f4ce546f77180903e7e93052ecad7419
+ languageName: node
+ linkType: hard
+
"ms@npm:2.0.0":
version: 2.0.0
resolution: "ms@npm:2.0.0"
@@ -20243,6 +20250,7 @@ __metadata:
merkletreejs: ^0.4.0
metamask-react: ^2.4.1
moment: ^2.29.4
+ mp4box: ^0.5.3
multiformats: ^12.1.3
osmojs: 16.9.0
papaparse: ^5.4.1