Skip to content

Commit

Permalink
feat: added asset owner validation before register ownership, added s…
Browse files Browse the repository at this point in the history
…ubtitles handle to videos
  • Loading branch information
Jadapema committed Jan 30, 2025
1 parent 55787f2 commit f541a6d
Show file tree
Hide file tree
Showing 12 changed files with 460 additions and 23 deletions.
16 changes: 13 additions & 3 deletions src/components/video-player/video-player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
MediaPlayerInstance,
MediaProvider,
useMediaState,
isHLSProvider,
isHLSProvider, Track
} from '@vidstack/react';
import { DefaultVideoLayout, defaultLayoutIcons } from '@vidstack/react/player/layouts/default';
import Label from '../label';
Expand All @@ -17,18 +17,25 @@ import '@vidstack/react/player/styles/default/theme.css';
import '@vidstack/react/player/styles/default/layouts/audio.css';
// @ts-ignore
import '@vidstack/react/player/styles/default/layouts/video.css';
import useGetSubtitles from '@src/hooks/use-get-subtitles.ts';

export type VideoPlayerProps = {
src: string;
cid: string;
titleMovie: string;
onBack?: () => void;
showBack?: boolean;
};

export const VideoPlayer: FC<VideoPlayerProps> = ({ src, titleMovie, onBack, showBack }) => {
export const VideoPlayer: FC<VideoPlayerProps> = ({ src, cid, titleMovie, onBack, showBack }) => {
const mdUp = useResponsive('up', 'md');
const player = useRef<MediaPlayerInstance>(null);
const controlsVisible = useMediaState('controlsVisible', player);
const { tracks, getSubtitles } = useGetSubtitles();

useEffect(() => {
if (cid) getSubtitles(cid);
}, [cid, getSubtitles]);

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
Expand Down Expand Up @@ -81,7 +88,7 @@ export const VideoPlayer: FC<VideoPlayerProps> = ({ src, titleMovie, onBack, sho
enableDateRangeMetadataCues: false,
enableMetadataCues: false,
enableID3MetadataCues: false,
enableWebVTT: false, // TODO change when subtitles needed
enableWebVTT: true,
enableIMSC1: false, // TODO change when subtitles needed
enableCEA708Captions: false, // TODO change when subtitles needed

Expand Down Expand Up @@ -188,6 +195,9 @@ export const VideoPlayer: FC<VideoPlayerProps> = ({ src, titleMovie, onBack, sho
)}
<MediaProvider />
<DefaultVideoLayout icons={defaultLayoutIcons} />
{tracks.map((track) => (
<Track key={track.src} {...track} />
))}
</MediaPlayer>
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/config/abi/AssetOwnership.json

Large diffs are not rendered by default.

104 changes: 104 additions & 0 deletions src/hooks/use-get-asset-owner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// REACT IMPORTS
import { useState, useCallback } from 'react';

// VIEM IMPORTS
import { Address } from 'viem';
import { publicClient } from '@src/clients/viem/publicClient';

// LOCAL IMPORTS
import AssetOwnershipAbi from '@src/config/abi/AssetOwnership.json';
import { GLOBAL_CONSTANTS } from '@src/config-global.ts';

// ----------------------------------------------------------------------

/**
* Interface for handling errors within the hook.
*/
interface GetAssetOwnerError {
message: string;
code?: number;
[key: string]: any;
}

/**
* Interface defining the structure of the hook's return value.
*/
interface UseGetAssetOwnerHook {
ownerAddress?: Address;
loading: boolean;
error?: GetAssetOwnerError | null;
fetchOwnerAddress: (assetIdHex: string) => Promise<Address | undefined>;
}

/**
* Hook to retrieve the owner's address of a specific asset.
*
* @returns {UseGetAssetOwnerHook} An object containing the owner's address, loading state, error information, and a function to fetch the owner's address.
*/
export const useGetAssetOwner = (): UseGetAssetOwnerHook => {
// State variables
const [ownerAddress, setOwnerAddress] = useState<Address | undefined>(undefined);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<GetAssetOwnerError | null>(null);

/**
* Converts a hexadecimal asset ID to decimal.
*
* @param hexId - The asset ID in hexadecimal format.
* @returns The asset ID in decimal format as a string.
*/
const convertHexToDecimal = (hexId: string): string => {
// Remove '0x' prefix if present
const cleanHex = hexId.startsWith('0x') ? hexId.slice(2) : hexId;
return BigInt(`0x${cleanHex}`).toString(10);
};

/**
* Fetches the owner's address of the asset.
*
* @param assetIdHex - The asset ID in hexadecimal format.
* @returns {Promise<Address | undefined>} The owner's address if successful, otherwise undefined.
*/
const fetchOwnerAddress = useCallback(async (assetIdHex: string): Promise<Address | undefined> => {
if (!assetIdHex) {
setError({ message: 'Asset ID is missing.' });
return undefined;
}

setLoading(true);
setError(null);

try {
// Convert assetId from hexadecimal to decimal
const assetIdDecimal = convertHexToDecimal(assetIdHex);

// Call the 'ownerOf' function on the AssetOwnership contract
const owner: any = await publicClient.readContract({
address: GLOBAL_CONSTANTS.ASSET_OWNERSHIP_ADDRESS,
abi: AssetOwnershipAbi.abi,
functionName: 'ownerOf',
args: [assetIdDecimal],
});

setOwnerAddress(owner);
setError(null);
return owner;
} catch (err: any) {
console.error('USE GET ASSET OWNER ERROR:', err);
setOwnerAddress(undefined);
setError({
message: err?.message || 'An error occurred while retrieving the asset owner.',
});
return undefined;
} finally {
setLoading(false);
}
}, []);

return {
ownerAddress,
loading,
error,
fetchOwnerAddress,
};
};
104 changes: 104 additions & 0 deletions src/hooks/use-get-subtitles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useCallback, useState } from 'react';
import useMetadata from '@src/hooks/use-metadata';

/**
* Represents a subtitle track configuration for media players.
*/
export interface SubtitleTrack {
src: string;
label: string;
language: string;
kind: 'subtitles';
default: boolean;
}

/**
* Return type of the useGetSubtitles hook
*/
export interface UseGetSubtitlesReturn {
/** Array of formatted subtitle tracks */
tracks: SubtitleTrack[];
/** Loading state indicator */
loading: boolean;
/** Function to fetch subtitles by CID */
getSubtitles: (cid: string) => Promise<SubtitleTrack[]>;
}

/**
* Maps common language names to standard language codes
*/
const getLanguageCode = (label: string): string => {
const normalized = label.trim().toLowerCase();
switch (normalized) {
case 'english':
return 'en-US';
case 'spanish':
return 'es-ES';
case 'french':
return 'fr-FR';
case 'german':
return 'de-DE';
case 'portuguese':
return 'pt-BR';
default:
return 'en-US'; // Fallback to English
}
};

/**
* Custom hook to fetch and format subtitle tracks for media players
*/
const useGetSubtitles = (): UseGetSubtitlesReturn => {
const { getMetadata } = useMetadata();
const [tracks, setTracks] = useState<SubtitleTrack[]>([]);
const [loading, setLoading] = useState(false);

/**
* Fetches and processes subtitles for a given CID
* @param cid Content identifier for the media
* @returns Formatted subtitle tracks
*/
const getSubtitles = useCallback(async (cid: string): Promise<SubtitleTrack[]> => {
setLoading(true);
try {
const metadata = await getMetadata(cid);
const subtitleAttachments = metadata.Data.attachments.filter(
(attachment) => attachment.type === 'text/vtt'
);

let hasDefault = false;
const processedTracks = subtitleAttachments.map((attachment) => {
const isEnglish = attachment.title.toLowerCase() === 'english';
const language = getLanguageCode(attachment.title);

const track: SubtitleTrack = {
src: `https://g.watchit.movie/content/${attachment.cid}/`,
label: attachment.title,
language,
kind: 'subtitles',
default: !hasDefault && isEnglish
};

if (track.default) hasDefault = true;
return track;
});

// Fallback to first track if no English found
if (!hasDefault && processedTracks.length > 0) {
processedTracks[0].default = true;
}

setTracks(processedTracks);
return processedTracks;
} catch (error) {
setTracks([]);
return [];
} finally {
setLoading(false);
}
}, [getMetadata]);

return { tracks, loading, getSubtitles };
};

export default useGetSubtitles;
83 changes: 83 additions & 0 deletions src/hooks/use-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useState, useCallback } from 'react';

/**
* Interface representing each attachment in the metadata.
*/
export interface Attachment {
cid: string;
type: string;
title: string;
description: string;
}

/**
* Interface representing the Data object within the metadata response.
*/
export interface MetadataData {
title: string;
description: string;
attachments: Attachment[];
custom_fields: any | null;
}

/**
* Interface representing the overall metadata response structure.
*/
export interface Metadata {
Type: string;
Data: MetadataData;
}

/**
* Interface representing the return value of the useMetadata hook.
*/
export interface UseMetadataReturn {
metadata: Metadata | null;
loading: boolean;
getMetadata: (cid: string) => Promise<Metadata>;
}

/**
* Custom React hook to fetch metadata from a specific URL using a CID.
*
* @returns {UseMetadataReturn} An object containing:
* - metadata: The fetched metadata or null.
* - loading: A boolean indicating the loading state.
* - getMetadata: An asynchronous function to fetch the metadata.
*/
const useMetadata = (): UseMetadataReturn => {
const [metadata, setMetadata] = useState<Metadata | null>(null);
const [loading, setLoading] = useState<boolean>(false);

/**
* Asynchronous function to fetch metadata using the provided CID.
*
* @param {string} cid - The CID used to construct the request URL.
* @returns {Promise<Metadata>} The fetched metadata.
* @throws Will throw an error if the fetch operation fails.
*/
const getMetadata = useCallback(async (cid: string): Promise<Metadata> => {
setLoading(true);
try {
const response = await fetch(`https://g.watchit.movie/metadata/${cid}/`);

if (!response.ok) {
throw new Error(`Error fetching metadata: ${response.statusText}`);
}

const data: Metadata = await response.json();
setMetadata(data);
return data;
} catch (error) {
console.error(error);
setMetadata(null);
throw error;
} finally {
setLoading(false);
}
}, []);

return { metadata, loading, getMetadata };
};

export default useMetadata;
6 changes: 2 additions & 4 deletions src/hooks/use-register-asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,14 @@ export const useRegisterAsset = (): UseRegisterAssetHook => {

try {
const toAddress = sessionData?.profile?.ownedBy.address;
// const asset = BigInt(assetId).toString(10)
const asset = assetId.replace(/^f0/, '0x');
if (!toAddress) {
throw new Error('The active account address was not found in the session.');
}

const registerAssetData = encodeFunctionData({
abi: AssetOwnershipAbi.abi,
functionName: 'registerAsset',
args: [toAddress, asset],
functionName: 'register',
args: [toAddress, assetId],
});

const calls = [
Expand Down
Loading

0 comments on commit f541a6d

Please sign in to comment.