using System; using System.IO; using System.Threading.Tasks; using Kill.Network; using UnityEngine; namespace Kill.Video { /// /// 下载 .264 -> 转封装 MP4(Android MediaCodec+MediaMuxer) -> 返回本地 mp4 路径 /// 不直接控制 VideoPlayer,由调用方拿到路径后自行 Init /// public class VideoDownloadPlayService { /// /// 完整流程:下载 → 转码 /// /// 视频源 url(.264 裸流) /// 下载进度回调(0~1) /// 下载完成,即将开始转码(可更新 UI 提示) /// (success, mp4Path) success=false 时 mp4Path 为 null/错误信息 public async Task DownloadConvertAndPlay( string url, Action onDownloadProgress, Action onConvertStart, Action onComplete) { if (string.IsNullOrEmpty(url)) { onComplete?.Invoke(false, "url is empty"); return false; } string mp4Path = VideoCacheManager.GetCachedMp4Path(url); // 1) 已缓存(mp4 直接缓存 / h264 转码后缓存),直接返回 if (File.Exists(mp4Path) && new FileInfo(mp4Path).Length > 0) { onComplete?.Invoke(true, mp4Path); return true; } // 2a) 链接是 .mp4:直接下载即得,无需转码 if (!IsH264Url(url)) { SafeDelete(mp4Path); HttpResponse respMp4 = null; try { respMp4 = await NetworkCtrl.Instance.DownloadFile(url, mp4Path, onDownloadProgress); } catch (Exception e) { Debug.LogError("[VideoDownloadPlayService] mp4 download exception: " + e); SafeDelete(mp4Path); onComplete?.Invoke(false, e.Message); return false; } if (respMp4 == null || !respMp4.IsSuccess || !File.Exists(mp4Path) || new FileInfo(mp4Path).Length <= 0) { SafeDelete(mp4Path); onComplete?.Invoke(false, "mp4 download fail: " + (respMp4?.ErrorMessage ?? "unknown")); return false; } onComplete?.Invoke(true, mp4Path); return true; } // 2b) 链接是 .h264:先下载再转码 string h264Path = VideoCacheManager.GetTempH264Path(url); SafeDelete(h264Path); SafeDelete(mp4Path); HttpResponse resp = null; try { resp = await NetworkCtrl.Instance.DownloadFile(url, h264Path, onDownloadProgress); } catch (Exception e) { Debug.LogError("[VideoDownloadPlayService] download exception: " + e); SafeDelete(h264Path); onComplete?.Invoke(false, e.Message); return false; } if (resp == null || !resp.IsSuccess || !File.Exists(h264Path)) { SafeDelete(h264Path); onComplete?.Invoke(false, "download fail: " + (resp?.ErrorMessage ?? "unknown")); return false; } onConvertStart?.Invoke(h264Path); // 3) 转码 .264 -> .mp4(ffmpeg-kit,libx264 重编码) bool convertOk = await FFmpegKitBridge.ConvertH264ToMp4Async(h264Path, mp4Path); if (!convertOk) { Debug.LogError("[VideoDownloadPlayService] ffmpeg convert fail"); } #region debug-point vf-2 (调试图: 记录源/产物文件大小 + 生成的 mp4 媒体信息) try { var hi = new FileInfo(h264Path); var mi = new FileInfo(mp4Path); Debug.Log("[vf-2] h264Size=" + (hi.Exists ? hi.Length.ToString() : "missing") + " mp4Size=" + (mi.Exists ? mi.Length.ToString() : "missing") + " convertOk=" + convertOk); if (mi.Exists && mi.Length > 0) { #if UNITY_ANDROID && !UNITY_EDITOR FFmpegKitBridge.ProbeMp4Info(mp4Path); #endif } } catch (Exception e) { Debug.LogError("[vf-2] " + e); } #endregion // 转码完成,删除 .h264 临时文件 SafeDelete(h264Path); // 失败时清理未完成 mp4 if (!convertOk || !File.Exists(mp4Path)) { SafeDelete(mp4Path); onComplete?.Invoke(false, "convert fail"); return false; } onComplete?.Invoke(true, mp4Path); return true; } private static void SafeDelete(string p) { if (string.IsNullOrEmpty(p)) return; try { if (File.Exists(p)) File.Delete(p); } catch { } } /// 判断链接是否为 .h264/.264 裸流(mp4 等其他一律按可直接下载处理) public static bool IsH264Url(string url) { if (string.IsNullOrEmpty(url)) return false; var l = url.ToLowerInvariant(); return l.EndsWith(".h264") || l.EndsWith(".264", StringComparison.Ordinal); } } }