From 44ac3f5ba5ee45a48d8ef5fd2d8852c18509d62d Mon Sep 17 00:00:00 2001 From: Tsanie Lily Date: Fri, 15 May 2020 22:49:38 +0800 Subject: [PATCH] feature: export video to gallery for iOS --- Pixiview/Illust/ViewIllustPage.xaml.cs | 64 ++++++++++++++- Pixiview/Resources/Languages/zh-CN.xml | 4 + Pixiview/Resources/ResourceHelper.cs | 4 + Pixiview/Utils/FileStore.cs | 16 ++++ Pixiview/Utils/IllustData.cs | 1 + Pixiview/Utils/Stores.cs | 28 +++++-- Pixiview/Utils/Ugoira.cs | 109 +++++++++++++++++++++++++ 7 files changed, 215 insertions(+), 11 deletions(-) diff --git a/Pixiview/Illust/ViewIllustPage.xaml.cs b/Pixiview/Illust/ViewIllustPage.xaml.cs index 01a383e..9875fb2 100644 --- a/Pixiview/Illust/ViewIllustPage.xaml.cs +++ b/Pixiview/Illust/ViewIllustPage.xaml.cs @@ -523,26 +523,37 @@ namespace Pixiview.Illust } var item = illusts[p]; - List extras = new List(); + 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(); + if (status != PermissionStatus.Granted) + { + status = await Permissions.RequestAsync(); + 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)) diff --git a/Pixiview/Resources/Languages/zh-CN.xml b/Pixiview/Resources/Languages/zh-CN.xml index f3b3e2d..5dafb2f 100644 --- a/Pixiview/Resources/Languages/zh-CN.xml +++ b/Pixiview/Resources/Languages/zh-CN.xml @@ -44,4 +44,8 @@ 请选择收藏夹操作 替换 合并 + 导出视频 + 无法导出视频,请先下载完成。 + 视频已保存,是否继续? + 视频已导出到照片库。 \ No newline at end of file diff --git a/Pixiview/Resources/ResourceHelper.cs b/Pixiview/Resources/ResourceHelper.cs index 7a800f0..2fc9224 100644 --- a/Pixiview/Resources/ResourceHelper.cs +++ b/Pixiview/Resources/ResourceHelper.cs @@ -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 dict = new Dictionary(); diff --git a/Pixiview/Utils/FileStore.cs b/Pixiview/Utils/FileStore.cs index a899800..a166831 100644 --- a/Pixiview/Utils/FileStore.cs +++ b/Pixiview/Utils/FileStore.cs @@ -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 SaveVideoToGalleryAsync(string file) + { + var task = new TaskCompletionSource(); + if (UIVideo.IsCompatibleWithSavedPhotosAlbum(file)) + { + UIVideo.SaveToPhotosAlbum(file, (path, err) => + { + task.SetResult(err?.ToString()); + }); + } + return task.Task; + } +#endif + public static Task SaveImageToGalleryAsync(ImageSource image) { #if __IOS__ diff --git a/Pixiview/Utils/IllustData.cs b/Pixiview/Utils/IllustData.cs index b579e56..5230626 100644 --- a/Pixiview/Utils/IllustData.cs +++ b/Pixiview/Utils/IllustData.cs @@ -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; diff --git a/Pixiview/Utils/Stores.cs b/Pixiview/Utils/Stores.cs index 7ab234a..a3253e1 100644 --- a/Pixiview/Utils/Stores.cs +++ b/Pixiview/Utils/Stores.cs @@ -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(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) { diff --git a/Pixiview/Utils/Ugoira.cs b/Pixiview/Utils/Ugoira.cs index 849e54c..969b913 100644 --- a/Pixiview/Utils/Ugoira.cs +++ b/Pixiview/Utils/Ugoira.cs @@ -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 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