Mobile Dev

Accessing Video Dimensions in expo-video: NaturalSize Alternative

Learn how to access video dimensions in expo-video without naturalSize. Implement Instagram-style pixel-perfect video players with proper dimension handling.

1 answer 1 view

How can I access the naturalSize property in expo-video instead of expo-av? I’m trying to implement a video player similar to Instagram where videos are displayed pixel perfect in a feed list, and when clicked, they should expand to their original size using naturalSize functionality. I need this implementation specifically for expo-video, not expo-av.

The expo-video library doesn’t provide a direct naturalSize property like expo-av; instead, you must access video dimensions through player.videoTrack.size after listening to the sourceLoad event. This approach allows you to capture the original video dimensions needed for implementing Instagram-style pixel-perfect video expansion in your feed layout, ensuring your videos render exactly as intended when expanded to their full size.


Contents


Understanding the API Differences Between expo-video and expo-av

The fundamental difference between expo-video and expo-av lies in their API design for video dimension handling. While expo-av’s <Video> component exposed a straightforward naturalSize prop that provided the original video dimensions, expo-video takes a more track-based approach that requires understanding its event system.

In expo-av, you could simply access videoRef.current?.naturalSize to get the original dimensions. However, expo-video has restructured this functionality to be more aligned with modern video streaming practices. This change reflects a shift toward more granular control over video tracks and their properties.

The key architectural difference is that expo-video separates video tracks from the main player component, allowing for more sophisticated handling of multiple video qualities, formats, and streams. This means you need to access dimensions through the video track rather than directly through the player.

When migrating from expo-av to expo-video, you’ll need to refactor your dimension access logic. The good news is that the new approach provides more flexibility and better performance, especially when dealing with different video resolutions or adaptive streaming formats.


Accessing Video Dimensions in expo-video: The Correct Approach

The correct method to access video dimensions in expo-video involves using the video track’s size property after the source has been loaded. This process requires understanding the event system and properly timing your dimension access.

The Event-Based Approach

Unlike expo-av’s synchronous naturalSize property, expo-video uses an event-driven approach. You need to listen for the sourceLoad event, which fires when the video source is successfully loaded and ready for playback. Only after this event can you safely access the video dimensions.

javascript
const player = useVideoPlayer();
const [videoDimensions, setVideoDimensions] = useState(null);

useEvent(player, 'sourceLoad', () => {
 setVideoDimensions(player.videoTrack?.size);
});

Accessing Through Video Track

The dimensions are stored in player.videoTrack.size, which provides an object with width and height properties. This object represents the original, uncropped dimensions of the video source.

javascript
const dimensions = player.videoTrack?.size;
if (dimensions) {
 console.log(`Original dimensions: ${dimensions.width}x${dimensions.height}`);
}

It’s important to note that the video track might not always be available immediately, which is why listening to the sourceLoad event is crucial. This event guarantees that both the player and the video track are properly initialized.

Handling Multiple Video Tracks

For more advanced implementations, expo-video supports multiple video tracks (like different resolutions). In such cases, you might want to access the dimensions of a specific track:

javascript
useEvent(player, 'sourceLoad', () => {
 if (player.videoTracks.length > 0) {
 const mainTrack = player.videoTracks[0];
 setVideoDimensions(mainTrack.size);
 }
});

This approach gives you more control when dealing with adaptive streaming or multiple quality versions of the same video.


Implementing Instagram-Style Video Player with expo-video

Creating an Instagram-style video player with expo-video requires careful handling of video dimensions and state management. The key challenge is displaying videos pixel-perfect in a feed layout while allowing them to expand to their original size when tapped.

State Management for Video Dimensions

You’ll need to store the original video dimensions for each video in your feed. This typically involves creating a data structure that associates each video with its dimensions:

javascript
const [videoDimensions, setVideoDimensions] = useState({});

const handleVideoLoad = (videoId, player) => {
 useEvent(player, 'sourceLoad', () => {
 setVideoDimensions(prev => ({
 ...prev,
 [videoId]: player.videoTrack?.size
 }));
 });
};

Pixel-Perfect Feed Display

To achieve pixel-perfect video rendering in your feed, you’ll need to calculate the correct aspect ratio and apply it to the VideoView component:

javascript
const getVideoStyle = (videoId, containerWidth) => {
 const dimensions = videoDimensions[videoId];
 if (!dimensions) return { width: containerWidth, aspectRatio: 9/16 };
 
 const aspectRatio = dimensions.width / dimensions.height;
 return {
 width: containerWidth,
 aspectRatio: aspectRatio
 };
};

Handling Video Expansion

When a video is tapped, you’ll want to expand it to its original size. This requires managing an expanded state and applying the original dimensions:

javascript
const [expandedVideoId, setExpandedVideoId] = useState(null);

const handleVideoPress = (videoId) => {
 setExpandedVideoId(expandedVideoId === videoId ? null : videoId);
};

const getExpandedVideoStyle = (videoId) => {
 const dimensions = videoDimensions[videoId];
 if (!dimensions || expandedVideoId !== videoId) return {};
 
 return {
 width: dimensions.width,
 height: dimensions.height
 };
};

Creating the Instagram Feed Layout

Putting it all together, your Instagram-style feed would look something like this:

javascript
const VideoFeedItem = ({ video }) => {
 const player = useVideoPlayer(video.source);
 const [isExpanded, setIsExpanded] = useState(false);
 const [dimensions, setDimensions] = useState(null);

 useEffect(() => {
 useEvent(player, 'sourceLoad', () => {
 setDimensions(player.videoTrack?.size);
 });
 }, [player]);

 const handlePress = () => {
 setIsExpanded(!isExpanded);
 };

 const videoStyle = dimensions ? {
 width: isExpanded ? dimensions.width : '100%',
 aspectRatio: dimensions ? dimensions.width / dimensions.height : 9/16
 } : {};

 return (
 <Pressable onPress={handlePress}>
 <VideoView 
 player={player} 
 style={videoStyle}
 contentFit="contain"
 />
 </Pressable>
 );
};

This implementation provides the core functionality needed for an Instagram-style video player with expo-video, including pixel-perfect rendering and expansion to original size.


Cross-Platform Considerations and Best Practices

When implementing video dimension handling in expo-video, it’s important to consider cross-platform differences and follow best practices to ensure consistent behavior across Android and iOS.

Platform-Specific Behavior Differences

As noted in the GitHub issue, there are platform-specific differences in how video dimensions are reported between Android and iOS. Android may sometimes report different dimensions compared to iOS, especially with certain video formats or encoding types.

To account for this, consider implementing platform-specific fallbacks:

javascript
const getSafeDimensions = (dimensions) => {
 if (!dimensions) return null;
 
 // Add validation to handle potential platform differences
 if (dimensions.width <= 0 || dimensions.height <= 0) {
 console.warn('Invalid video dimensions detected');
 return null;
 }
 
 return dimensions;
};

Performance Considerations

Accessing video dimensions involves additional processing, especially when dealing with high-resolution videos. To optimize performance:

  1. Cache dimensions when possible to avoid repeated calculations
  2. Use memoization for style objects that depend on dimensions
  3. Consider debouncing rapid dimension updates in complex layouts
javascript
const memoizedVideoStyle = useMemo(() => {
 return getVideoStyle(videoId, containerWidth);
}, [videoId, containerWidth, videoDimensions[videoId]]);

Error Handling

Robust error handling is essential when working with video dimensions:

javascript
const handleVideoLoad = (player) => {
 try {
 useEvent(player, 'sourceLoad', () => {
 try {
 const dimensions = player.videoTrack?.size;
 if (dimensions) {
 setVideoDimensions(dimensions);
 } else {
 console.warn('Could not retrieve video dimensions');
 }
 } catch (error) {
 console.error('Error accessing video dimensions:', error);
 }
 });
 } catch (error) {
 console.error('Error setting up video player:', error);
 }
};

Memory Management

Properly managing video player instances is crucial for performance:

javascript
useEffect(() => {
 return () => {
 if (player) {
 player.pause();
 player.dispose();
 }
 };
}, [player]);

These cross-platform considerations and best practices will help ensure your Instagram-style video player works reliably across different platforms and devices.


Complete Code Example for Pixel-Perfect Video Expansion

Here’s a complete, working example of an Instagram-style video player using expo-video that demonstrates how to access video dimensions and implement pixel-perfect expansion:

javascript
import React, { useState, useEffect, useMemo } from 'react';
import { View, Pressable, StyleSheet } from 'react-native';
import { VideoView, useVideoPlayer, useEvent } from 'expo-video';

const InstagramVideoFeed = ({ videos }) => {
 const [expandedVideoId, setExpandedVideoId] = useState(null);
 const [videoDimensions, setVideoDimensions] = useState({});
 const [containerWidth, setContainerWidth] = useState(0);

 const handleLayout = (event) => {
 setContainerWidth(event.nativeEvent.layout.width);
 };

 const handleVideoPress = (videoId) => {
 setExpandedVideoId(expandedVideoId === videoId ? null : videoId);
 };

 const getVideoStyle = (videoId) => {
 const dimensions = videoDimensions[videoId];
 const isExpanded = expandedVideoId === videoId;
 
 if (!dimensions) {
 return {
 width: containerWidth,
 aspectRatio: 9/16,
 };
 }
 
 if (isExpanded) {
 return {
 width: dimensions.width,
 height: dimensions.height,
 };
 }
 
 return {
 width: containerWidth,
 aspectRatio: dimensions.width / dimensions.height,
 };
 };

 return (
 <View style={styles.container} onLayout={handleLayout}>
 {videos.map((video) => (
 <VideoFeedItem
 key={video.id}
 video={video}
 isExpanded={expandedVideoId === video.id}
 onPress={() => handleVideoPress(video.id)}
 getVideoStyle={getVideoStyle}
 />
 ))}
 </View>
 );
};

const VideoFeedItem = ({ video, isExpanded, onPress, getVideoStyle }) => {
 const player = useVideoPlayer(video.source);
 const [dimensions, setDimensions] = useState(null);
 
 // Set up event listener for sourceLoad
 useEvent(player, 'sourceLoad', () => {
 if (player.videoTrack?.size) {
 setDimensions(player.videoTrack.size);
 }
 });

 // Clean up player when component unmounts
 useEffect(() => {
 return () => {
 player.pause();
 player.dispose();
 };
 }, [player]);

 const videoStyle = getVideoStyle(video.id);

 return (
 <Pressable onPress={onPress} style={styles.videoContainer}>
 <VideoView 
 player={player} 
 style={[styles.video, videoStyle]}
 contentFit="contain"
 isMuted
 isLooping
 useNativeControls={false}
 />
 </Pressable>
 );
};

const styles = StyleSheet.create({
 container: {
 flex: 1,
 },
 videoContainer: {
 marginBottom: 8,
 },
 video: {
 backgroundColor: 'black',
 },
});

export default InstagramVideoFeed;

Implementation Notes

  1. Video Player Setup: Each video in the feed gets its own useVideoPlayer instance, which is properly cleaned up when the component unmounts.

  2. Dimension Tracking: The sourceLoad event listener captures the original video dimensions when they become available.

  3. Expansion Logic: When a video is tapped, it expands to its original dimensions if it’s not already expanded, or collapses back to feed size if it is.

  4. Responsive Design: The video styles are calculated based on the container width and the original aspect ratio, ensuring pixel-perfect rendering in the feed.

  5. Performance Optimization: The useMemo hook could be used to memoize style calculations, though in this example we’ve kept it simple for clarity.

This complete example provides everything you need to implement an Instagram-style video feed with expo-video, including the ability to access original video dimensions without relying on the deprecated naturalSize property from expo-av.


Sources

  1. Expo Video Documentation — Official guide for expo-video API and implementation: https://docs.expo.dev/versions/latest/sdk/video/
  2. Expo GitHub Issue — Discussion of platform-specific differences in video dimension handling: https://github.com/expo/expo/issues/14677

Conclusion

Implementing Instagram-style video players with expo-video requires understanding that there’s no direct equivalent to the naturalSize property from expo-av. Instead, you must access video dimensions through player.videoTrack.size after listening to the sourceLoad event. This approach gives you more control over video tracks and enables pixel-perfect video expansion in your feed layout. By following the event-based dimension access pattern demonstrated in this guide, you can successfully transition from expo-av to expo-video while maintaining the same functionality for your video application.

Authors
Verified by moderation
Moderation
Accessing Video Dimensions in expo-video: NaturalSize Alternative