TranscodeHelpers.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System; 6 using System.IO; 7 using System.Linq; 8 using System.Threading; 9 using System.Threading.Tasks; 10 11 using AdvancedPaste.Models; 12 using ManagedCommon; 13 using Windows.ApplicationModel.DataTransfer; 14 using Windows.Media.MediaProperties; 15 using Windows.Media.Transcoding; 16 using Windows.Storage; 17 18 namespace AdvancedPaste.Helpers; 19 20 internal static class TranscodeHelpers 21 { 22 public static async Task<DataPackage> TranscodeToMp3Async(DataPackageView clipboardData, CancellationToken cancellationToken, IProgress<double> progress) => 23 await TranscodeMediaAsync(clipboardData, MediaEncodingProfile.CreateMp3(AudioEncodingQuality.High), ".mp3", cancellationToken, progress); 24 25 public static async Task<DataPackage> TranscodeToMp4Async(DataPackageView clipboardData, CancellationToken cancellationToken, IProgress<double> progress) => 26 await TranscodeMediaAsync(clipboardData, MediaEncodingProfile.CreateMp4(VideoEncodingQuality.HD1080p), ".mp4", cancellationToken, progress); 27 28 private static async Task<DataPackage> TranscodeMediaAsync(DataPackageView clipboardData, MediaEncodingProfile baseOutputProfile, string extension, CancellationToken cancellationToken, IProgress<double> progress) 29 { 30 Logger.LogTrace(); 31 32 var inputFiles = await clipboardData.GetStorageItemsAsync(); 33 34 if (inputFiles.Count != 1) 35 { 36 throw new InvalidOperationException($"{nameof(TranscodeMediaAsync)} does not support multiple files"); 37 } 38 39 var inputFile = inputFiles.Single() as StorageFile ?? throw new InvalidOperationException($"{nameof(TranscodeMediaAsync)} only supports files"); 40 var inputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(inputFile.Path); 41 42 var inputProfile = await MediaEncodingProfile.CreateFromFileAsync(inputFile); 43 var outputProfile = CreateOutputProfile(inputProfile, baseOutputProfile); 44 45 #if DEBUG 46 static string ProfileToString(MediaEncodingProfile profile) => System.Text.Json.JsonSerializer.Serialize(profile, options: new() { WriteIndented = true }); 47 Logger.LogDebug($"{nameof(inputProfile)}: {ProfileToString(inputProfile)}"); 48 Logger.LogDebug($"{nameof(outputProfile)}: {ProfileToString(outputProfile)}"); 49 #endif 50 51 var outputFolder = await Task.Run(() => Directory.CreateTempSubdirectory("PowerToys_AdvancedPaste_"), cancellationToken); 52 var outputFileName = StringComparer.OrdinalIgnoreCase.Equals(Path.GetExtension(inputFile.Path), extension) ? inputFileNameWithoutExtension + "_1" : inputFileNameWithoutExtension; 53 var outputFilePath = Path.Combine(outputFolder.FullName, Path.ChangeExtension(outputFileName, extension)); 54 await File.WriteAllBytesAsync(outputFilePath, [], cancellationToken); // TranscodeAsync seems to require the output file to exist 55 56 await TranscodeMediaAsync(inputFile, await StorageFile.GetFileFromPathAsync(outputFilePath), outputProfile, cancellationToken, progress); 57 58 return await DataPackageHelpers.CreateFromFileAsync(outputFilePath); 59 } 60 61 private static MediaEncodingProfile CreateOutputProfile(MediaEncodingProfile inputProfile, MediaEncodingProfile baseOutputProfile) 62 { 63 MediaEncodingProfile outputProfile = new() 64 { 65 Video = null, 66 Audio = null, 67 }; 68 69 outputProfile.Container = baseOutputProfile.Container.Copy(); 70 71 if (inputProfile.Video != null && baseOutputProfile.Video != null) 72 { 73 outputProfile.Video = baseOutputProfile.Video.Copy(); 74 75 if (inputProfile.Video.Bitrate != 0) 76 { 77 outputProfile.Video.Bitrate = inputProfile.Video.Bitrate; 78 } 79 80 if (inputProfile.Video.FrameRate.Numerator != 0) 81 { 82 outputProfile.Video.FrameRate.Numerator = inputProfile.Video.FrameRate.Numerator; 83 } 84 85 if (inputProfile.Video.FrameRate.Denominator != 0) 86 { 87 outputProfile.Video.FrameRate.Denominator = inputProfile.Video.FrameRate.Denominator; 88 } 89 90 if (inputProfile.Video.PixelAspectRatio.Numerator != 0) 91 { 92 outputProfile.Video.PixelAspectRatio.Numerator = inputProfile.Video.PixelAspectRatio.Numerator; 93 } 94 95 if (inputProfile.Video.PixelAspectRatio.Denominator != 0) 96 { 97 outputProfile.Video.PixelAspectRatio.Denominator = inputProfile.Video.PixelAspectRatio.Denominator; 98 } 99 100 outputProfile.Video.Width = inputProfile.Video.Width; 101 outputProfile.Video.Height = inputProfile.Video.Height; 102 } 103 104 if (inputProfile.Audio != null && baseOutputProfile.Audio != null) 105 { 106 outputProfile.Audio = baseOutputProfile.Audio.Copy(); 107 108 if (inputProfile.Audio.Bitrate != 0) 109 { 110 outputProfile.Audio.Bitrate = inputProfile.Audio.Bitrate; 111 } 112 113 if (inputProfile.Audio.BitsPerSample != 0) 114 { 115 outputProfile.Audio.BitsPerSample = inputProfile.Audio.BitsPerSample; 116 } 117 118 if (inputProfile.Audio.ChannelCount != 0) 119 { 120 outputProfile.Audio.ChannelCount = inputProfile.Audio.ChannelCount; 121 } 122 123 if (inputProfile.Audio.SampleRate != 0) 124 { 125 outputProfile.Audio.SampleRate = inputProfile.Audio.SampleRate; 126 } 127 } 128 129 return outputProfile; 130 } 131 132 private static async Task TranscodeMediaAsync(StorageFile inputFile, StorageFile outputFile, MediaEncodingProfile outputProfile, CancellationToken cancellationToken, IProgress<double> progress) 133 { 134 if (outputProfile.Video == null && outputProfile.Audio == null) 135 { 136 throw new InvalidOperationException("Target profile does not contain media"); 137 } 138 139 async Task<PrepareTranscodeResult> GetPrepareResult(bool hardwareAccelerationEnabled) 140 { 141 MediaTranscoder transcoder = new() 142 { 143 AlwaysReencode = false, 144 HardwareAccelerationEnabled = hardwareAccelerationEnabled, 145 }; 146 147 return await transcoder.PrepareFileTranscodeAsync(inputFile, outputFile, outputProfile); 148 } 149 150 var prepareResult = await GetPrepareResult(hardwareAccelerationEnabled: true); 151 152 if (!prepareResult.CanTranscode) 153 { 154 Logger.LogWarning($"Unable to transcode with hardware acceleration enabled, falling back to software; {nameof(prepareResult.FailureReason)}={prepareResult.FailureReason}"); 155 156 prepareResult = await GetPrepareResult(hardwareAccelerationEnabled: false); 157 } 158 159 if (!prepareResult.CanTranscode) 160 { 161 var message = ResourceLoaderInstance.ResourceLoader.GetString(prepareResult.FailureReason == TranscodeFailureReason.CodecNotFound ? "TranscodeErrorUnsupportedCodec" : "TranscodeErrorGeneral"); 162 throw new PasteActionException(message, new InvalidOperationException($"Error transcoding; {nameof(prepareResult.FailureReason)}={prepareResult.FailureReason}")); 163 } 164 165 await prepareResult.TranscodeAsync().AsTask(cancellationToken, progress); 166 } 167 }