feature: export video to gallery for iOS

This commit is contained in:
Tsanie Lily 2020-05-15 22:49:38 +08:00
parent 8ce5942662
commit 44ac3f5ba5
7 changed files with 215 additions and 11 deletions

View File

@ -523,26 +523,37 @@ namespace Pixiview.Illust
}
var item = illusts[p];
List<string> extras = new List<string>();
var share = ResourceHelper.Share;
var preview = Stores.GetPreviewImagePath(item.PreviewUrl);
if (preview != null)
{
extras.Add(share);
}
var userDetail = ResourceHelper.UserDetail;
extras.Add(userDetail);
var related = ResourceHelper.RelatedIllusts;
extras.Add(related);
var saveOriginal = ResourceHelper.SaveOriginal;
#if __IOS__
var exportVideo = ResourceHelper.ExportVideo;
if (IsAnimateSliderVisible)
{
extras.Add(exportVideo);
}
#endif
var saveOriginal = ResourceHelper.SaveOriginal;
var illustItem = IllustItem;
var result = await DisplayActionSheet(
$"{illustItem.Title} (id: {illustItem.Id})",
ResourceHelper.Cancel,
saveOriginal,
extras.ToArray());
if (result == saveOriginal)
{
SaveOriginalImage(item);
@ -565,8 +576,57 @@ namespace Pixiview.Illust
var page = new RelatedIllustsPage(illustItem);
await Navigation.PushAsync(page);
}
#if __IOS__
else if (result == exportVideo)
{
ExportVideo();
}
#endif
}
#if __IOS__
private async void ExportVideo()
{
string msg = ResourceHelper.CantExportVideo;
if (ugoira != null && ugoiraData != null && ugoiraData.body != null)
{
if (Stores.CheckUgoiraVideo(ugoiraData.body.originalSrc))
{
var flag = await DisplayAlert(ResourceHelper.Operation,
ResourceHelper.AlreadySavedVideo,
ResourceHelper.Yes,
ResourceHelper.No);
if (!flag)
{
return;
}
}
var status = await Permissions.CheckStatusAsync<Permissions.Photos>();
if (status != PermissionStatus.Granted)
{
status = await Permissions.RequestAsync<Permissions.Photos>();
if (status != PermissionStatus.Granted)
{
App.DebugPrint("access denied to gallery.");
return;
}
}
var success = await Task.Run(ugoira.ExportVideo);
if (success != null)
{
var result = await FileStore.SaveVideoToGalleryAsync(success);
msg = result ?? ResourceHelper.ExportSuccess;
}
}
await DisplayAlert(ResourceHelper.Title, msg, ResourceHelper.Ok);
}
#endif
private async void SaveOriginalImage(IllustDetailItem item)
{
if (Stores.CheckIllustImage(item.OriginalUrl))

View File

@ -44,4 +44,8 @@
<FavoritesOperation>请选择收藏夹操作</FavoritesOperation>
<FavoritesReplace>替换</FavoritesReplace>
<FavoritesCombine>合并</FavoritesCombine>
<ExportVideo>导出视频</ExportVideo>
<CantExportVideo>无法导出视频,请先下载完成。</CantExportVideo>
<AlreadySavedVideo>视频已保存,是否继续?</AlreadySavedVideo>
<ExportSuccess>视频已导出到照片库。</ExportSuccess>
</root>

View File

@ -28,6 +28,10 @@ namespace Pixiview.Resources
public static string FavoritesOperation => GetResource(nameof(FavoritesOperation));
public static string FavoritesReplace => GetResource(nameof(FavoritesReplace));
public static string FavoritesCombine => GetResource(nameof(FavoritesCombine));
public static string ExportVideo => GetResource(nameof(ExportVideo));
public static string CantExportVideo => GetResource(nameof(CantExportVideo));
public static string AlreadySavedVideo => GetResource(nameof(AlreadySavedVideo));
public static string ExportSuccess => GetResource(nameof(ExportSuccess));
static readonly Dictionary<string, LanguageResource> dict = new Dictionary<string, LanguageResource>();

View File

@ -1,6 +1,7 @@
using System.Threading.Tasks;
using Xamarin.Forms;
#if __IOS__
using UIKit;
using Xamarin.Forms.Platform.iOS;
#elif __ANDROID__
using Android.Content;
@ -13,6 +14,21 @@ namespace Pixiview.Utils
{
public class FileStore
{
#if __IOS__
public static Task<string> SaveVideoToGalleryAsync(string file)
{
var task = new TaskCompletionSource<string>();
if (UIVideo.IsCompatibleWithSavedPhotosAlbum(file))
{
UIVideo.SaveToPhotosAlbum(file, (path, err) =>
{
task.SetResult(err?.ToString());
});
}
return task.Task;
}
#endif
public static Task<string> SaveImageToGalleryAsync(ImageSource image)
{
#if __IOS__

View File

@ -266,6 +266,7 @@ namespace Pixiview.Utils
public string file;
public int delay;
public string FilePath;
public bool Incompleted;
public int First;
public int Last;

View File

@ -100,19 +100,19 @@ namespace Pixiview.Utils
}
}
public static ImageSource LoadUgoiraImage(string zip, string frame)
public static string LoadUgoiraImage(string zip, string frame)
{
var file = Path.Combine(PersonalFolder, ugoiraFolder, zip, frame);
if (File.Exists(file))
{
try
{
return ImageSource.FromFile(file);
}
catch (Exception ex)
{
App.DebugError("load.ugoira", $"failed to load ugoira frame: {zip}/{frame}, error: {ex.Message}");
}
//try
//{
return file;
//}
//catch (Exception ex)
//{
// App.DebugError("load.ugoira", $"failed to load ugoira frame: {zip}/{frame}, error: {ex.Message}");
//}
}
return null;
}
@ -138,6 +138,11 @@ namespace Pixiview.Utils
}
}
public static string GetUgoiraVideoPath(string url)
{
return Path.Combine(PersonalFolder, ugoiraFolder, Path.GetFileNameWithoutExtension(url) + ".mp4");
}
private static T ReadObject<T>(string file)
{
string content = null;
@ -397,6 +402,11 @@ namespace Pixiview.Utils
var file = Path.Combine(PersonalFolder, imageFolder, Path.GetFileName(url));
return File.Exists(file);
}
public static bool CheckUgoiraVideo(string url)
{
var file = Path.Combine(PersonalFolder, ugoiraFolder, Path.GetFileNameWithoutExtension(url) + ".mp4");
return File.Exists(file);
}
public static string GetPreviewImagePath(string url)
{

View File

@ -6,6 +6,15 @@ using System.Threading;
using System.Threading.Tasks;
using Pixiview.Illust;
using Xamarin.Forms;
using System.Linq;
#if __IOS__
using AVFoundation;
using CoreGraphics;
using CoreMedia;
using CoreVideo;
using Foundation;
using UIKit;
#endif
namespace Pixiview.Utils
{
@ -148,6 +157,7 @@ namespace Pixiview.Utils
var image = Stores.LoadUgoiraImage(zip, frame.file);
if (image != null)
{
frame.FilePath = image;
frames[i] = image;
}
else
@ -297,6 +307,7 @@ namespace Pixiview.Utils
var file = ExtractImage(zip, data, frame.Offset);
if (file != null)
{
frame.FilePath = file;
frames[i] = ImageSource.FromFile(file);
}
}
@ -382,6 +393,104 @@ namespace Pixiview.Utils
Done = false;
}
}
#if __IOS__
public async Task<string> ExportVideo()
{
if (ugoira == null || ugoira.frames.Any(f => f.Incompleted))
{
return null;
}
var file = Stores.GetUgoiraVideoPath(ugoira.originalSrc);
if (File.Exists(file))
{
File.Delete(file);
}
var fileURL = NSUrl.FromFilename(file);
var images = ugoira.frames.Select(f => UIImage.FromFile(f.FilePath)).ToArray();
var videoSettings = new NSMutableDictionary
{
{ AVVideo.CodecKey, AVVideo.CodecH264 },
{ AVVideo.WidthKey, NSNumber.FromNFloat(images[0].Size.Width) },
{ AVVideo.HeightKey, NSNumber.FromNFloat(images[0].Size.Height) }
};
var videoWriter = new AVAssetWriter(fileURL, AVFileType.Mpeg4, out var err);
if (err != null)
{
App.DebugError("export.video", $"failed to create an AVAssetWriter: {err}");
return null;
}
var writerInput = new AVAssetWriterInput(AVMediaType.Video, new AVVideoSettingsCompressed(videoSettings));
var sourcePixelBufferAttributes = new NSMutableDictionary
{
{ CVPixelBuffer.PixelFormatTypeKey, NSNumber.FromInt32((int)CVPixelFormatType.CV32ARGB) }
};
var pixelBufferAdaptor = new AVAssetWriterInputPixelBufferAdaptor(writerInput, sourcePixelBufferAttributes);
videoWriter.AddInput(writerInput);
if (videoWriter.StartWriting())
{
videoWriter.StartSessionAtSourceTime(CMTime.Zero);
var lastTime = CMTime.Zero;
for (int i = 0; i < images.Length; i++)
{
while (true)
{
if (writerInput.ReadyForMoreMediaData)
{
// get pixel buffer and fill it with the image
using (CVPixelBuffer pixelBufferImage = pixelBufferAdaptor.PixelBufferPool.CreatePixelBuffer())
{
pixelBufferImage.Lock(CVPixelBufferLock.None);
try
{
IntPtr pxdata = pixelBufferImage.BaseAddress;
if (pxdata != IntPtr.Zero)
{
using (var rgbColorSpace = CGColorSpace.CreateDeviceRGB())
{
var cgImage = images[i].CGImage;
var width = cgImage.Width;
using (CGBitmapContext bitmapContext = new CGBitmapContext(pxdata, width, cgImage.Height,
8, 4 * (width + width % 8), rgbColorSpace, CGImageAlphaInfo.NoneSkipFirst))
{
if (bitmapContext != null)
{
bitmapContext.DrawImage(new CGRect(0, 0, cgImage.Width, cgImage.Height), cgImage);
}
}
}
}
}
finally
{
pixelBufferImage.Unlock(CVPixelBufferLock.None);
}
// and finally append buffer to adapter
if (pixelBufferAdaptor.AssetWriterInput.ReadyForMoreMediaData && pixelBufferImage != null)
{
pixelBufferAdaptor.AppendPixelBufferWithPresentationTime(pixelBufferImage, lastTime);
}
}
var frameTime = new CMTime(ugoira.frames[i].delay, 1000);
lastTime = CMTime.Add(lastTime, frameTime);
break;
}
Thread.Sleep(100);
}
}
writerInput.MarkAsFinished();
await videoWriter.FinishWritingAsync();
return file;
}
return null;
}
#endif
}
public class UgoiraEventArgs : EventArgs