223 lines
5.4 KiB
TypeScript
223 lines
5.4 KiB
TypeScript
// 兼容性状态类型
|
||
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)));
|
||
} |