expo-popcore-old/utils/video-utils.ts

223 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 兼容性状态类型
interface AVPlaybackStatus {
isLoaded: boolean;
isPlaying?: boolean;
durationMillis?: number;
naturalSize?: {
width: number;
height: number;
};
positionMillis?: number;
}
export interface VideoMetadata {
width: number;
height: number;
duration: number;
aspectRatio: number;
orientation: 'portrait' | 'landscape' | 'square';
}
/**
* 从AVPlaybackStatus中提取视频元数据
*/
export function extractVideoMetadata(status: AVPlaybackStatus): VideoMetadata | null {
if (!status.isLoaded) {
return null;
}
// 尝试从不同的属性获取视频尺寸
let width = 0, height = 0;
// 检查是否有naturalSize属性
if ('naturalSize' in status && status.naturalSize) {
const naturalSize = status.naturalSize as any;
width = naturalSize.width || 0;
height = naturalSize.height || 0;
}
// 如果没有获取到尺寸,使用默认值
if (width === 0 || height === 0) {
width = 1920; // 默认宽度
height = 1080; // 默认高度
}
const duration = status.durationMillis ? status.durationMillis / 1000 : 0;
const aspectRatio = width > 0 ? width / height : 1;
// 判断视频方向
let orientation: 'portrait' | 'landscape' | 'square';
if (Math.abs(width - height) < 10) {
orientation = 'square';
} else if (width > height) {
orientation = 'landscape';
} else {
orientation = 'portrait';
}
return {
width,
height,
duration,
aspectRatio,
orientation,
};
}
/**
* 根据容器尺寸和视频元数据计算最佳显示尺寸
*/
export function calculateOptimalVideoSize(
containerWidth: number,
maxHeight: number,
metadata: VideoMetadata,
resizeMode: 'contain' | 'cover' | 'stretch' = 'contain'
): { width: number; height: number } {
const { aspectRatio } = metadata;
switch (resizeMode) {
case 'contain':
// 保持宽高比,完全显示在容器内
if (containerWidth / aspectRatio <= maxHeight) {
return {
width: containerWidth,
height: containerWidth / aspectRatio,
};
} else {
return {
width: maxHeight * aspectRatio,
height: maxHeight,
};
}
case 'cover':
// 保持宽高比,填满容器(可能裁剪)
if (containerWidth / aspectRatio >= maxHeight) {
return {
width: containerWidth,
height: containerWidth / aspectRatio,
};
} else {
return {
width: maxHeight * aspectRatio,
height: maxHeight,
};
}
case 'stretch':
// 拉伸填满容器
return {
width: containerWidth,
height: maxHeight,
};
default:
return {
width: containerWidth,
height: containerWidth / aspectRatio,
};
}
}
/**
* 格式化视频时长
*/
export function formatVideoDuration(seconds: number): string {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60);
if (minutes === 0) {
return `${remainingSeconds}`;
} else if (remainingSeconds === 0) {
return `${minutes}分钟`;
} else {
return `${minutes}${remainingSeconds}`;
}
}
/**
* 格式化视频尺寸信息
*/
export function formatVideoSize(metadata: VideoMetadata): string {
const { width, height } = metadata;
if (width >= 1000 || height >= 1000) {
return `${(width / 1000).toFixed(1)}k × ${(height / 1000).toFixed(1)}k`;
} else {
return `${width} × ${height}`;
}
}
/**
* 检测视频文件类型
*/
export function getVideoFileType(url: string): string | null {
if(!url) return null;
const extension = url.split('.').pop()?.toLowerCase();
const videoExtensions = [
'mp4', 'webm', 'ogg', 'mov', 'avi',
'mkv', 'flv', 'wmv', 'm4v', '3gp'
];
return extension && videoExtensions.includes(extension) ? extension : null;
}
/**
* 生成视频封面图的URI如果需要
*/
export function generateVideoPosterUrl(videoUrl: string): string {
// 某些CDN支持通过参数生成封面图
// 这里可以根据实际使用的视频服务进行调整
if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) {
// YouTube视频封面图逻辑
const videoId = extractYouTubeVideoId(videoUrl);
return videoId ? `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg` : '';
}
// 其他视频服务的封面图逻辑可以在这里添加
return '';
}
/**
* 从YouTube URL提取视频ID
*/
function extractYouTubeVideoId(url: string): string | null {
const regex = /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/;
const match = url.match(regex);
return match ? match[1] : null;
}
/**
* 验证视频URL是否有效
*/
export function isValidVideoUrl(url: string): boolean {
if (!url || typeof url !== 'string') {
return false;
}
try {
const urlObj = new URL(url);
const isValidProtocol = ['http:', 'https:', 'ftp:'].includes(urlObj.protocol);
const hasVideoExtension = getVideoFileType(url) !== null;
return isValidProtocol && (hasVideoExtension || url.includes('stream'));
} catch {
return false;
}
}
/**
* 获取推荐的缩略图时间点视频的10%、30%、60%位置)
*/
export function getThumbnailTimepoints(duration: number): number[] {
if (duration <= 0) return [1];
const points = [
duration * 0.1,
duration * 0.3,
duration * 0.6,
];
return points.map(t => Math.max(1, Math.floor(t)));
}