/ src / modules / AdvancedPaste / AdvancedPaste / Helpers / TranscodeHelpers.cs
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  }