В приложении, которое мы пишем, пользователь должен иметь возможность выбирать и отправлять мультимедийные вложения, в том числе видео, записанное на его устройстве.Затем эти видео необходимо просматривать с помощью веб-браузеров в других приложениях и, следовательно, должно быть MP4 (не MOV).
Моя первая попытка конвертировать клипы использует PHImageManager.RequestExportSession
, и она работает, но мы получаем MP4 в кодировке H.265, который все еще не поддерживается многими браузерами, и поэтому вместо этого нам нужно кодирование H.264.Я не нашел способа настроить сеанс экспорта для выбора указанной кодировки, поэтому я вместо этого попробовал подход AVAsset
, адаптировав код, размещенный @SushiHangover здесь .
Моя PHImageManager.RequestExportSession
конверсия выглядит следующим образом:
// note: MediaBase is my own abstraction of media (AV and photos)
private void convertVideo(MediaBase mediaBase, MediaConversionOptions options, TaskCompletionSource<Uri> tcs)
{
if (!(mediaBase.InternalObject is PHAsset asset))
return;
var videoOptions = makeVideoRequestOptions(options, out var exportPreset);
if (options.ProgressHandler != null)
videoOptions.ProgressHandler = onProgress;
PHImageManager.DefaultManager.RequestExportSession(
asset,
videoOptions,
exportPreset,
(session, info) =>
{
session.OutputUrl = NSUrl.FromFilename(options.OutputPath ?? getDefaultOutputPath());
session.OutputFileType = getFileTypeFrom(options.OutputType);
session.ExportAsynchronously(() =>
{
switch (session.Status)
{
case AVAssetExportSessionStatus.Failed:
var err = session.Error?.Description;
Crashes.TrackError(new Exception($"Could not convert movie to format: {options.OutputType}. {err}"));
tcs.SetResult(null);
break;
case AVAssetExportSessionStatus.Completed:
tcs.SetResult(new Uri(session.OutputUrl.AbsoluteString));
break;
}
});
});
void onProgress(double progress, NSError error, out bool stop, NSDictionary info)
{
Exception ex = error != null ? new Exception(error.Description) : null;
Dictionary<string, object> dict = null; // consider supporting 'info' in Video conversion session
options.ProgressHandler(progress, ex, out stop, dict);
}
string getDefaultOutputPath()
{
var outputFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
var file = $"{Guid.NewGuid().ToString("D")}{options.OutputType.GetFileExtension()}";
return Path.Combine(outputFolder, file);
}
}
... и это AVAsset
подход:
private void convertVideo2(MediaBase mediaBase, MediaConversionOptions options, string url, TaskCompletionSource<Uri> tcs)
{
// adapted from SushiHangover's code:
// https://stackoverflow.com/questions/38262302/xamarin-lowering-video-size
if (!(mediaBase.InternalObject is PHAsset asset))
return;
try
{
var filename = new FileInfo(url).Name;
var avAsset = AVAsset.FromUrl(new NSUrl(url));
var reader = AVAssetReader.FromAsset(avAsset, out var assetreaderError); // todo Handle error
var assetTrack = avAsset.Tracks.FirstOrDefault();
if (assetTrack == null)
return;
Size dimensions = options.Dimensions ?? mediaBase.Dimensions;
var inputSettings = new AVVideoSettingsUncompressed
{
Height = (int) dimensions.Width,
Width = (int) dimensions.Height
};
var readerOutput = new AVAssetReaderTrackOutput(assetTrack, inputSettings)
{
AlwaysCopiesSampleData = false
};
var outFile = new FileInfo(options.OutputPath ?? Path.Combine(Path.GetTempPath(), $"_tmp_{filename}"));
var extension = options.OutputType.GetFileExtension();
outFile = outFile.ReplaceExtension(extension);
if (outFile.Exists)
{
outFile.Delete();
}
var tempUrl = NSUrl.FromFilename(outFile.FullName);
var writer = new AVAssetWriter(tempUrl, AVFileType.Mpeg4, out var assetWriterError);
var outputSettings = new AVVideoSettingsCompressed
{
Height = (int)dimensions.Width,
Width = (int)dimensions.Height,
Codec = AVVideoCodec.H264, // todo honor Codec specified by options
CodecSettings = new AVVideoCodecSettings { AverageBitRate = 1000000 } // todo honor bitrate specified by options
};
var writerInput = new AVAssetWriterInput(AVMediaType.Video, outputSettings)
{
ExpectsMediaDataInRealTime = false
};
writer.AddInput(writerInput);
writer.StartWriting();
reader.AddOutput(readerOutput);
reader.StartReading();
writer.StartSessionAtSourceTime(CoreMedia.CMTime.Zero);
var queue = new DispatchQueue("mediaInputQueue");
writerInput.RequestMediaData(queue, () =>
{
var isMoppedUp = false;
while (writerInput.ReadyForMoreMediaData)
{
var nextBuffer = readerOutput.CopyNextSampleBuffer();
if (nextBuffer != null)
{
writerInput.AppendSampleBuffer(nextBuffer);
continue;
}
mopup(); // <-- never reached. Seems writerInput.ReadyForMoreMediaData gets flipped to false
break;
}
mopup();
tcs.SetResult(new Uri(outFile.FullName));
void mopup()
{
if (isMoppedUp)
return;
writerInput.MarkAsFinished();
writer.FinishWritingAsync();
reader.CancelReading();
reader.Dispose();
writer.Dispose();
writerInput.Dispose();
}
});
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Video conversion ERROR: {ex}");
}
}
Подводя итогвопросы:
- Есть ли способ указать предпочтительный кодек с помощью
PHImageManager.RequestExportSession
API? - У кого-нибудь есть какие-либо советы о том, что может быть причиной для
AVAssetWriterInput.ReadyForMoreMediaData
возврата false, видимо, сокращает конверсию?(результат обычно составляет всего несколько кадров).