在我们编写的应用程序中,用户需要能够选择和提交媒体附件,包括用她的设备录制的视频。然后,需要使用其他应用程序中的网络浏览器观看这些视频,因此这些视频必须为MP4(而非MOV)。
我第一次尝试转换剪辑使用PHImageManager.RequestExportSession
,它可以工作,但是我们得到了H.265编码的MP4,许多浏览器仍然不支持,因此,我们需要H.264编码。我还没有找到一种配置导出会话以选择指定编码的方法,因此我改编了@SushiHangover here发布的代码,尝试了AVAsset
方法。
我基于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显然会缩短转换时间的原因有何提示? (结果通常只有几帧)。