iPhone端React应用视频上传异常:视频被识别为image/jpeg
iPhone视频上传Bug排查与解决方案
我在React应用里碰到个棘手问题:iPhone用户上传视频时总是失败,但Windows桌面端、安卓设备和iPad都完全正常。排查后发现关键异常:在iPhone上选择明明是视频的文件时,文件名后缀会变成.jpeg,file.type显示为image/jpeg,只有文件大小和原视频一致。
原始代码参考
Input元素
<input hidden multiple accept="video/mp4,video/avi,video/quicktime,video/x-ms-wmv,image/*" style={{ display: "none" }} id="raised-button-file" type="file" onChange={handleMedia} />
handleMedia处理函数
const handleMedia = (e) => { const files = e.target.files; const allowedVideoExtensions = ["mp4", "avi", "mov", "wmv"]; for (let i = 0; i < files.length; i++) { const file = files[i]; const fileExtension = file.name.split('.').pop().toLowerCase(); const isVideo = allowedVideoExtensions.includes(fileExtension); const isImage = !isVideo; // 不是视频就当作图片 alert(`File Name: ${file.name}, Extension: ${fileExtension}, Is Video: ${isVideo}, fileType: ${file.type}, fileSize: ${file.size / 1024 / 1024}`); if (isImage && file.size > 20 * 1024 * 1024) { // 20 MB alert("One or more of your images exceed the 20MB size limit. Please select smaller files."); return; } else if (isVideo && file.size > 200 * 1024 * 1024) { // 200 MB alert("One or more of your videos exceed the 200MB size limit. Please select smaller files."); return; } } setMedia(Array.from(e.target.files)); };
简化测试组件(问题复现用)
import React from "react"; import { Flexbox, FlexboxCol, Typo } from "../../UI/StyledComponents"; import CameraAltIcon from "@mui/icons-material/CameraAlt"; const TestComponent = () => { const handleMedia = (e) => { const files = e.target.files; const allowedVideoExtensions = ["mp4", "avi", "mov", "wmv"]; for (let i = 0; i < files.length; i++) { const file = files[i]; const fileExtension = file.name.split(".").pop().toLowerCase(); const isVideo = allowedVideoExtensions.includes(fileExtension); const isImage = !isVideo; // 不是视频就当作图片 alert( `File Name: ${file.name}, Extension: ${fileExtension}, Is Video: ${isVideo}, fileType: ${file.type}, fileSize: ${(file.size / 1024 / 1024).toFixed(2)}MB` ); } }; return ( <FlexboxCol fWidth fHeight withScrollbar hideScroll fullPadding={24}> <FlexboxCol fWidth fullPadding={8} sx={{ border: "1px solid #00ace8" }}> <Typo sx={{ fontSize: ".75rem", color: '#fff', backgroundColor: '#000', direction: 'ltr', whiteSpace: "pre-wrap", }}> accept="video/mp4,video/avi,video/quicktime,video/x-ms-wmv,image/*" </Typo> <Flexbox fWidth> <input hidden multiple accept="video/mp4,video/avi,video/quicktime,video/x-ms-wmv,image/*" style={{ display: "none" }} id="raised-button-file" type="file" onChange={handleMedia} /> <label htmlFor="raised-button-file" style={{ height: 28, width: 28 }}> <CameraAltIcon sx={{ mx: 0.4 }} style={{ color: "#00ace880", height: 28, width: 28 }} /> </label> </Flexbox> </FlexboxCol> <FlexboxCol fWidth fullPadding={8} sx={{ border: "1px solid #00ace8" }}> <Typo sx={{ fontSize: ".75rem", color: '#fff', backgroundColor: '#000', direction: 'ltr', whiteSpace: "pre-wrap", }}>accept="video/*,image/*"</Typo> <Flexbox fWidth> <input hidden multiple accept="video/*,image/*" style={{ display: "none" }} id="raised-button-file" type="file" onChange={handleMedia} /> <label htmlFor="raised-button-file" style={{ height: 28, width: 28 }}> <CameraAltIcon sx={{ mx: 0.4 }} style={{ color: "#FF5733", height: 28, width: 28 }} /> </label> </Flexbox> </FlexboxCol> <FlexboxCol fWidth fullPadding={8} sx={{ border: "1px solid #00ace8" }}> <Typo sx={{ fontSize: ".75rem", color: '#fff', backgroundColor: '#000', direction: 'ltr', whiteSpace: "pre-wrap", }}>accept="video/*"</Typo> <Flexbox fWidth> <input hidden multiple accept="video/*" style={{ display: "none" }} id="raised-button-file" type="file" onChange={handleMedia} /> <label htmlFor="raised-button-file" style={{ height: 28, width: 28 }}> <CameraAltIcon sx={{ mx: 0.4 }} style={{ color: "#33FFBD", height: 28, width: 28 }} /> </label> </Flexbox> </FlexboxCol> <FlexboxCol fWidth fullPadding={8} sx={{ border: "1px solid #00ace8" }}> <Typo sx={{ fontSize: ".75rem", color: '#fff', backgroundColor: '#000', direction: 'ltr', whiteSpace: "pre-wrap", }}> accept="video/mp4,video/x-m4v,video/*" </Typo> <Flexbox fWidth> <input hidden multiple accept="video/mp4,video/x-m4v,video/*" style={{ display: "none" }} id="raised-button-file" type="file" onChange={handleMedia} /> <label htmlFor="raised-button-file" style={{ height: 28, width: 28 }}> <CameraAltIcon sx={{ mx: 0.4 }} style={{ color: "#9037af", height: 28, width: 28 }} /> </label> </Flexbox> </FlexboxCol> </FlexboxCol> ); }; export default TestComponent;
问题原因
这是iOS Safari的特性:当通过照片库选择视频时,系统可能返回视频的缩略图文件而非原始视频,尤其是在accept属性同时包含图片和视频类型时,文件选择器会优先返回预览图。另外iOS的命名逻辑会把视频缩略图命名成和原视频同名但后缀为.jpeg的文件,导致代码误判成图片。
解决方案
- 放弃用文件名后缀判断类型:直接通过
file.type判断,因为后缀在iOS上不可靠。检查file.type是否以video/开头来识别视频,以image/开头识别图片。 - 优化
accept属性:如果不需要同时上传图片和视频,分开做两个独立的上传按钮;如果必须同时支持,用accept="video/*,image/*",但要确保代码逻辑依赖MIME类型而非后缀。 - 修改
handleMedia函数:
const handleMedia = (e) => { const files = e.target.files; for (let i = 0; i < files.length; i++) { const file = files[i]; const isVideo = file.type.startsWith('video/'); const isImage = file.type.startsWith('image/'); alert(`File Name: ${file.name}, Is Video: ${isVideo}, fileType: ${file.type}, fileSize: ${file.size / 1024 / 1024}`); if (isImage && file.size > 20 * 1024 * 1024) { // 20 MB alert("部分图片超过20MB大小限制,请选择更小的文件。"); return; } else if (isVideo && file.size > 200 * 1024 * 1024) { // 200 MB alert("部分视频超过200MB大小限制,请选择更小的文件。"); return; } } setMedia(Array.from(e.target.files)); };
额外提示:如果用户是从照片库选择视频,iOS有时会提供「编辑」选项,编辑后的视频可能会改变文件类型和命名,这时候也需要依赖MIME类型判断。
内容的提问来源于stack exchange,提问作者Jordan Alexander
相关产品推荐
相关产品推荐

