Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(feed): video duration calculated from Video component #1405

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
57 changes: 48 additions & 9 deletions packages/components/video/VideoCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { AVPlaybackStatus } from "expo-av";

Check failure on line 1 in packages/components/video/VideoCard.tsx

View workflow job for this annotation

GitHub Actions / check-js

'AVPlaybackStatus' is defined but never used
import { isEqual } from "lodash";
import React, { memo, useState } from "react";
import MP4Box from "mp4box";

Check failure on line 3 in packages/components/video/VideoCard.tsx

View workflow job for this annotation

GitHub Actions / check-js

'MP4Box' is defined but never used
import React, { memo, useEffect, useState } from "react";

Check failure on line 4 in packages/components/video/VideoCard.tsx

View workflow job for this annotation

GitHub Actions / check-js

'useEffect' is defined but never used
import {
StyleProp,
StyleSheet,
Expand Down Expand Up @@ -30,7 +32,10 @@
} 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";
Expand All @@ -40,7 +45,9 @@
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;
Expand All @@ -56,6 +63,7 @@
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") {
Expand All @@ -71,13 +79,26 @@
? 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 (
<BrandText style={[fontSemibold13, { color: errorColor }]}>
Video not found
</BrandText>
);

return (
<View
style={[
Expand Down Expand Up @@ -116,13 +137,8 @@
]}
/>

<View style={imgDurationBoxStyle}>
<BrandText style={fontSemibold13}>
{prettyMediaDuration(video.videoFile.videoMetadata?.duration)}
</BrandText>
</View>

{video?.location && (
<VideoDuration video={video} />
{video?.location ? (
<View style={positionButtonBoxStyle}>
<LocationButton
onPress={() =>
Expand All @@ -135,6 +151,8 @@
stroke={neutralFF}
/>
</View>
) : (
<View />
)}
</CustomPressable>

Expand Down Expand Up @@ -208,6 +226,27 @@
);
}, isEqual);

function VideoDuration({ video }: { video: SocialFeedVideoMetadata }) {
const { duration, isLoading } = useVideoAudioDuration(
web3ToWeb2URI(video?.videoFile.url),
video?.videoFile.videoMetadata?.duration || 0,
);

return (
<>
{!isLoading ? (
<View style={imgDurationBoxStyle}>
<BrandText style={fontSemibold13}>
{prettyMediaDuration(duration)}
</BrandText>
</View>
) : (
<View />
)}
</>
);
}

const imgBoxStyle: ViewStyle = {
position: "relative",
};
Expand Down
24 changes: 24 additions & 0 deletions packages/hooks/feed/useVideoAudioDuration.tsx
Original file line number Diff line number Diff line change
@@ -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 };
};
43 changes: 43 additions & 0 deletions packages/utils/types/mp4box.d.ts
Original file line number Diff line number Diff line change
@@ -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;
}
42 changes: 40 additions & 2 deletions packages/utils/video/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import MP4Box, { MP4ArrayBuffer, MP4BoxFile, MP4FileInfo } from "mp4box";

import { VideoFileMetadata } from "../types/files";

const getVideoDuration = (file: File) => {

Check failure on line 5 in packages/utils/video/index.ts

View workflow job for this annotation

GitHub Actions / check-js

'getVideoDuration' is assigned a value but never used
let duration = 0;
try {
const video = document.createElement("video");
Expand All @@ -19,11 +21,47 @@
} 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<VideoFileMetadata> => {
const fileBuffer = await file.arrayBuffer();
const duration = await getVideoDurationFromBuffer(fileBuffer);

return {
duration,
};
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
Loading