diff --git a/SentryReplay.Tests/PackageManagerTests.cs b/SentryReplay.Tests/PackageManagerTests.cs new file mode 100644 index 0000000..230095f --- /dev/null +++ b/SentryReplay.Tests/PackageManagerTests.cs @@ -0,0 +1,29 @@ +using System.Runtime.InteropServices; +using Shouldly; + +namespace SentryReplay.Tests; + +/// +/// Tests for PackageManager functionality. +/// +public class PackageManagerTests +{ + [Fact] + public void GetFFmpegDownloadUrl_ReturnsValidUrl() + { + // We can't directly test the private method, but we can verify the logic indirectly + // by checking that the current architecture is supported + var architecture = RuntimeInformation.ProcessArchitecture; + + // Verify that the architecture is one we support + architecture.ShouldBeOneOf(Architecture.X64, Architecture.Arm64, Architecture.X86, Architecture.Arm); + } + + [Fact] + public void FindFFmpegDirectories_ReturnsEnumerable() + { + // Test that the method returns an enumerable (even if empty) + var directories = PackageManager.FindFFmpegDirectories("."); + directories.ShouldNotBeNull(); + } +} diff --git a/SentryReplay/PackageManager.cs b/SentryReplay/PackageManager.cs index 50bd70a..d07b4e3 100644 --- a/SentryReplay/PackageManager.cs +++ b/SentryReplay/PackageManager.cs @@ -1,6 +1,7 @@ using System.IO; using System.IO.Compression; using System.Net.Http; +using System.Runtime.InteropServices; using Serilog; namespace SentryReplay; @@ -9,35 +10,82 @@ public static class PackageManager { private static async Task DownloadFile(string url, string savePath) { - using var client = new HttpClient(); - var response = await client.GetAsync(url); - if (response.IsSuccessStatusCode) - { - using var fileStream = File.Create(savePath); - await response.Content.CopyToAsync(fileStream); - } + using var client = new HttpClient { Timeout = TimeSpan.FromMinutes(5) }; + var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); + response.EnsureSuccessStatusCode(); + + using var fileStream = File.Create(savePath); + await response.Content.CopyToAsync(fileStream); } private static void ExtractZipFile(string zipFilePath, string extractPath) { - ZipFile.ExtractToDirectory(zipFilePath, extractPath, true); + // Extract and flatten nested directories - the zip contains a bin directory with ffmpeg.exe and required DLLs + using var archive = ZipFile.OpenRead(zipFilePath); + + foreach (var entry in archive.Entries) + { + if (string.IsNullOrEmpty(entry.Name)) + continue; + + // Find bin directory in the zip structure + var entryPath = entry.FullName.Replace('/', Path.DirectorySeparatorChar); + var binDirectory = "bin" + Path.DirectorySeparatorChar; + var binIndex = entryPath.IndexOf(binDirectory, StringComparison.OrdinalIgnoreCase); + + if (binIndex >= 0) + { + // Extract files from bin directory to root of extractPath + var relativePath = entryPath.Substring(binIndex + binDirectory.Length); + var destPath = Path.Combine(extractPath, relativePath); + + Directory.CreateDirectory(Path.GetDirectoryName(destPath)!); + entry.ExtractToFile(destPath, overwrite: true); + } + } } public static async Task DownloadAndExtractFFmpeg() { var outputFolder = Path.GetFullPath("ffmpeg"); - var url = "https://github.com/GyanD/codexffmpeg/releases/download/7.0/ffmpeg-7.0-full_build-shared.zip"; // TODO: ARM64 builds? + var url = GetFFmpegDownloadUrl(); var tempPath = Path.GetTempFileName(); - Log.Information("Getting ffmpeg"); + try + { + Log.Information("Downloading FFmpeg..."); - Log.Debug($"Downloading ffmpeg to {tempPath} from {url}"); - await DownloadFile(url, tempPath); + Log.Debug($"Downloading FFmpeg to {tempPath} from {url}"); + await DownloadFile(url, tempPath); - Log.Debug($"Extracting ffmpeg to {outputFolder}"); - ExtractZipFile(tempPath, outputFolder); + Log.Information("Extracting FFmpeg..."); + Log.Debug($"Extracting FFmpeg to {outputFolder}"); + + if (Directory.Exists(outputFolder)) + Directory.Delete(outputFolder, true); + Directory.CreateDirectory(outputFolder); + + ExtractZipFile(tempPath, outputFolder); + + Log.Information("FFmpeg downloaded and extracted successfully"); + } + finally + { + if (File.Exists(tempPath)) + File.Delete(tempPath); + } + } - File.Delete(tempPath); + private static string GetFFmpegDownloadUrl() + { + var architecture = RuntimeInformation.ProcessArchitecture; + + return architecture switch + { + Architecture.Arm64 => "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-winarm64-gpl-shared.zip", + Architecture.X64 => "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl-shared.zip", + _ => throw new PlatformNotSupportedException($"Unsupported architecture: {architecture}. Only x64 and ARM64 are supported.") + }; } public static IEnumerable FindFFmpegDirectories(string searchDirectory = ".")