feature: export video to gallery for iOS
This commit is contained in:
parent
8ce5942662
commit
44ac3f5ba5
@ -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))
|
||||
|
@ -44,4 +44,8 @@
|
||||
<FavoritesOperation>请选择收藏夹操作</FavoritesOperation>
|
||||
<FavoritesReplace>替换</FavoritesReplace>
|
||||
<FavoritesCombine>合并</FavoritesCombine>
|
||||
<ExportVideo>导出视频</ExportVideo>
|
||||
<CantExportVideo>无法导出视频,请先下载完成。</CantExportVideo>
|
||||
<AlreadySavedVideo>视频已保存,是否继续?</AlreadySavedVideo>
|
||||
<ExportSuccess>视频已导出到照片库。</ExportSuccess>
|
||||
</root>
|
@ -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>();
|
||||
|
||||
|
@ -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__
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user