移动端Chrome中HTML5 Download与PHP下载功能失效求助
首先,咱们先梳理下你遇到的核心问题:桌面端不管是<a download>还是PHP下载脚本都能正常工作,但在iPhone的Chrome里,<a>标签直接打开视频播放,PHP脚本打开后显示无法播放的叉号,完全触发不了下载行为。结合你提供的代码和排查信息,我整理了几个关键的问题点和修复方案:
1. 临时文件删除时机错误(致命坑点)
你第一个脚本里的unlink($save)写在了exit;之后,这行代码永远不会被执行,会导致服务器上堆积大量临时文件。而第二个脚本里把unlink($save)放在了readfile($save)之后、die()之前——这时候文件还在被PHP进程读取,直接删除会导致文件读取中断,移动端Chrome对这种中断的处理就是显示无法播放的错误。
修复方案:用register_shutdown_function延迟删除临时文件,确保文件读取完成后再清理:
// 替换原来的unlink位置,放在exit之前 register_shutdown_function(function() use ($save) { @unlink($save); // @忽略删除失败的警告,避免影响脚本执行 });
2. Content-Disposition格式不符合移动端规范
iPhone Chrome对Content-Disposition的格式要求更严格,尤其是文件名的编码和引号格式。你当前的写法可能存在兼容性问题,建议使用RFC 5987标准的filename*格式,同时保留兼容的filename:
// 替换原来的Content-Disposition header $encodedFilename = urlencode($nameNew); header("Content-Disposition: attachment; filename=\"$nameNew\"; filename*=UTF-8''$encodedFilename");
这个格式能确保移动端正确识别文件名,避免因为编码问题导致下载触发失败。
3. 输出缓冲与压缩的彻底清理
移动端浏览器对输出的完整性要求极高,任何多余的输出(比如缓冲里的空格、BOM)都会导致下载失效。你需要确保彻底清理输出缓冲,并且关闭zlib压缩:
// 在脚本最开头就关闭压缩 if (ini_get('zlib.output_compression')) { ini_set('zlib.output_compression', 'Off'); } // 彻底清理所有缓冲层级 while (ob_get_level() > 0) { ob_end_clean(); }
4. 远程视频文件的获取与校验缺失
你用file_put_contents($save, fopen($nameOld, 'r'))获取远程视频的方式没有错误处理,如果远程文件下载失败(比如网络波动),$save会是一个空文件或损坏文件,移动端Chrome遇到损坏的视频就会显示无法播放,而非触发下载。
修复方案:添加分块写入和文件完整性校验:
$remoteFile = fopen($nameOld, 'r'); if (!$remoteFile) { die('无法获取远程视频'); } $localFile = fopen($save, 'w'); if (!$localFile) { fclose($remoteFile); die('无法创建本地临时文件'); } // 分块写入,避免内存溢出 while (!feof($remoteFile)) { fwrite($localFile, fread($remoteFile, 8192)); } fclose($remoteFile); fclose($localFile); // 校验文件大小是否匹配 $headers = get_headers($nameOld, 1); $remoteSize = isset($headers['Content-Length']) ? (int)$headers['Content-Length'] : 0; $localSize = filesize($save); if ($remoteSize > 0 && $remoteSize !== $localSize) { unlink($save); die('视频文件下载不完整'); }
5. 针对iPhone Chrome的Header优化
结合iOS浏览器特性,调整Header配置,强制触发下载而非播放:
set_time_limit(0); header('Connection: Keep-Alive'); header('Content-Description: File Transfer'); // 优先用application/octet-stream,避免浏览器识别为视频直接播放 header('Content-Type: application/octet-stream'); // 上面提到的标准Content-Disposition格式 $encodedFilename = urlencode($nameNew); header("Content-Disposition: attachment; filename=\"$nameNew\"; filename*=UTF-8''$encodedFilename"); header('Content-Transfer-Encoding: binary'); header('Expires: 0'); header('Cache-Control: no-cache, must-revalidate'); header('Pragma: public'); header('Content-Length: ' . $localSize);
最终完整修复后的脚本示例
<?php // 关闭压缩 if (ini_get('zlib.output_compression')) { ini_set('zlib.output_compression', 'Off'); } // 清理所有输出缓冲 while (ob_get_level() > 0) { ob_end_clean(); } $nameOld = 'https://my-other-server.online/path/to/file.mp4'; $save = '/var/path/to/file.mp4'; $nameNew = "download.mp4"; // 下载远程文件到本地,带错误处理 $remoteFile = fopen($nameOld, 'r'); if (!$remoteFile) { die('无法获取远程视频'); } $localFile = fopen($save, 'w'); if (!$localFile) { fclose($remoteFile); die('无法创建本地临时文件'); } while (!feof($remoteFile)) { fwrite($localFile, fread($remoteFile, 8192)); } fclose($remoteFile); fclose($localFile); // 校验文件完整性 $headers = get_headers($nameOld, 1); $remoteSize = isset($headers['Content-Length']) ? (int)$headers['Content-Length'] : 0; $localSize = filesize($save); if ($remoteSize > 0 && $remoteSize !== $localSize) { unlink($save); die('视频文件下载不完整'); } // 设置下载Header set_time_limit(0); header('Connection: Keep-Alive'); header('Content-Description: File Transfer'); header('Content-Type: application/octet-stream'); $encodedFilename = urlencode($nameNew); header("Content-Disposition: attachment; filename=\"$nameNew\"; filename*=UTF-8''$encodedFilename"); header('Content-Transfer-Encoding: binary'); header('Expires: 0'); header('Cache-Control: no-cache, must-revalidate'); header('Pragma: public'); header('Content-Length: ' . $localSize); // 发送文件内容 readfile($save); // 注册脚本结束后删除临时文件 register_shutdown_function(function() use ($save) { @unlink($save); }); exit; ?>
另外,针对<a download>标签在iPhone Chrome失效的问题,你可以尝试添加type="application/octet-stream"属性:
<a href="your-download-script.php" download="download.mp4" type="application/octet-stream">下载视频</a>
最后提醒:如果你的服务器开启了CDN或缓存服务,需要确保下载脚本的请求不被缓存,避免旧的Header被返回。
内容的提问来源于stack exchange,提问作者730wavy

