Save media

This commit is contained in:
Justin Mazzocchi 2021-01-17 22:03:32 -08:00
parent 493ea98f13
commit 2eddf8c558
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
6 changed files with 112 additions and 4 deletions

View file

@ -0,0 +1,54 @@
// Copyright © 2021 Metabolist. All rights reserved.
import AVFoundation
enum AssetExportError: Error {
case exportSetup
case export
}
extension AVURLAsset {
func exportWithoutAudioTrack(completion: @escaping ((Result<URL, AssetExportError>) -> Void)) {
let composition = AVMutableComposition()
let exportDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
guard let sourceVideoTrack = tracks(withMediaType: .video).first,
let compositionVideoTrack = composition.addMutableTrack(
withMediaType: .video,
preferredTrackID: kCMPersistentTrackID_Invalid),
case .success = Result(catching: {
try compositionVideoTrack.insertTimeRange(
CMTimeRange(start: .zero, duration: duration),
of: sourceVideoTrack, at: .zero)
}),
let exportSession = AVAssetExportSession(
asset: composition,
presetName: AVAssetExportPresetHighestQuality),
exportSession.supportedFileTypes.contains(.mp4),
case .success = Result(catching: {
try FileManager.default.createDirectory(
at: exportDirectory,
withIntermediateDirectories: false)
})
else {
completion(.failure(.exportSetup))
return
}
let exportURL = exportDirectory.appendingPathComponent(url.lastPathComponent)
exportSession.outputFileType = AVFileType.mp4
exportSession.outputURL = exportURL
exportSession.timeRange = CMTimeRange(start: .zero, duration: duration)
exportSession.exportAsynchronously {
guard exportSession.status == .completed else {
completion(.failure(.export))
return
}
completion(.success(exportURL))
}
}
}

View file

@ -39,6 +39,7 @@
"attachment.edit.thumbnail.prompt" = "Drag the circle on the preview to choose the focal point which will always be in view on all thumbnails"; "attachment.edit.thumbnail.prompt" = "Drag the circle on the preview to choose the focal point which will always be in view on all thumbnails";
"attachment.sensitive-content" = "Sensitive content"; "attachment.sensitive-content" = "Sensitive content";
"attachment.media-hidden" = "Media hidden"; "attachment.media-hidden" = "Media hidden";
"attachment.unable-to-export-media" = "Unable to export media";
"bookmarks" = "Bookmarks"; "bookmarks" = "Bookmarks";
"camera-access.title" = "Camera access needed"; "camera-access.title" = "Camera access needed";
"camera-access.description" = "Open system settings to allow camera access"; "camera-access.description" = "Open system settings to allow camera access";

View file

@ -51,6 +51,7 @@
D059373E25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D059373D25AB8D5200754FDF /* CompositionPollOptionView.swift */; }; D059373E25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D059373D25AB8D5200754FDF /* CompositionPollOptionView.swift */; };
D059373F25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D059373D25AB8D5200754FDF /* CompositionPollOptionView.swift */; }; D059373F25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D059373D25AB8D5200754FDF /* CompositionPollOptionView.swift */; };
D059376125ABE2E800754FDF /* XMLUnescaper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D059376025ABE2E800754FDF /* XMLUnescaper.swift */; }; D059376125ABE2E800754FDF /* XMLUnescaper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D059376025ABE2E800754FDF /* XMLUnescaper.swift */; };
D05E688525B55AE8001FB2C6 /* AVURLAsset+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05E688425B55AE8001FB2C6 /* AVURLAsset+Extensions.swift */; };
D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; }; D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; };
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; }; D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; };
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BC5E525202AD90079541D /* ProfileViewController.swift */; }; D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BC5E525202AD90079541D /* ProfileViewController.swift */; };
@ -218,6 +219,7 @@
D059373225AAEA7000754FDF /* CompositionPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionPollView.swift; sourceTree = "<group>"; }; D059373225AAEA7000754FDF /* CompositionPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionPollView.swift; sourceTree = "<group>"; };
D059373D25AB8D5200754FDF /* CompositionPollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionPollOptionView.swift; sourceTree = "<group>"; }; D059373D25AB8D5200754FDF /* CompositionPollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionPollOptionView.swift; sourceTree = "<group>"; };
D059376025ABE2E800754FDF /* XMLUnescaper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLUnescaper.swift; sourceTree = "<group>"; }; D059376025ABE2E800754FDF /* XMLUnescaper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLUnescaper.swift; sourceTree = "<group>"; };
D05E688425B55AE8001FB2C6 /* AVURLAsset+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVURLAsset+Extensions.swift"; sourceTree = "<group>"; };
D0625E58250F092900502611 /* StatusListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusListCell.swift; sourceTree = "<group>"; }; D0625E58250F092900502611 /* StatusListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusListCell.swift; sourceTree = "<group>"; };
D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentConfiguration.swift; sourceTree = "<group>"; }; D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentConfiguration.swift; sourceTree = "<group>"; };
D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@ -569,6 +571,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D01C6FAB252024BD003D0300 /* Array+Extensions.swift */, D01C6FAB252024BD003D0300 /* Array+Extensions.swift */,
D05E688425B55AE8001FB2C6 /* AVURLAsset+Extensions.swift */,
D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */, D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */,
D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */, D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */,
D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */, D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */,
@ -834,6 +837,7 @@
D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */, D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */,
D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */, D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */, D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */,
D05E688525B55AE8001FB2C6 /* AVURLAsset+Extensions.swift in Sources */,
D036AA02254B6101009094DF /* NotificationListCell.swift in Sources */, D036AA02254B6101009094DF /* NotificationListCell.swift in Sources */,
D08B8D42253F92B600B1EBEF /* ImagePageViewController.swift in Sources */, D08B8D42253F92B600B1EBEF /* ImagePageViewController.swift in Sources */,
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */, D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */,

View file

@ -2,10 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>NSMicrophoneUsageDescription</key>
<string>Enables Metatext to take videos and add them to your posts.</string>
<key>NSCameraUsageDescription</key>
<string>Enables Metatext to take photos and videos and add them to your posts.</string>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
@ -24,6 +20,14 @@
<string>1</string> <string>1</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSCameraUsageDescription</key>
<string>Enables Metatext to take photos and videos and add them to your posts</string>
<key>NSMicrophoneUsageDescription</key>
<string>Enables Metatext to take videos and add them to your posts</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Enables Metatext to add items to your Photo Library</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Enables Metatext to access photos from your library and add them to your posts</string>
<key>UIApplicationSceneManifest</key> <key>UIApplicationSceneManifest</key>
<dict> <dict>
<key>UIApplicationSupportsMultipleScenes</key> <key>UIApplicationSupportsMultipleScenes</key>

View file

@ -48,6 +48,12 @@ final class ImagePageViewController: UIPageViewController {
systemItem: .close, systemItem: .close,
primaryAction: UIAction { [weak self] _ in self?.presentingViewController?.dismiss(animated: true) }) primaryAction: UIAction { [weak self] _ in self?.presentingViewController?.dismiss(animated: true) })
navigationItem.rightBarButtonItem = .init(
systemItem: .action,
primaryAction: UIAction { [weak self] _ in
(self?.viewControllers?.first as? ImageViewController)?.presentActivityViewController()
})
navigationController?.barHideOnTapGestureRecognizer.addTarget( navigationController?.barHideOnTapGestureRecognizer.addTarget(
self, self,
action: #selector(toggleDescriptionVisibility)) action: #selector(toggleDescriptionVisibility))

View file

@ -1,5 +1,6 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import AVFoundation
import Kingfisher import Kingfisher
import UIKit import UIKit
import ViewModels import ViewModels
@ -146,6 +147,44 @@ extension ImageViewController {
self.descriptionBackgroundView.alpha = self.descriptionBackgroundView.alpha > 0 ? 0 : 1 self.descriptionBackgroundView.alpha = self.descriptionBackgroundView.alpha > 0 ? 0 : 1
} }
} }
func presentActivityViewController() {
if let image = imageView.image {
let activityViewController = UIActivityViewController(activityItems: [image], applicationActivities: [])
present(activityViewController, animated: true)
} else if let asset = playerView.player?.currentItem?.asset as? AVURLAsset {
asset.exportWithoutAudioTrack { result in
DispatchQueue.main.async {
switch result {
case let .success(url):
let activityViewController = UIActivityViewController(
activityItems: [url],
applicationActivities: [])
activityViewController.completionWithItemsHandler = { _, _, _, _ in
try? FileManager.default.removeItem(at: url.deletingLastPathComponent())
}
self.present(activityViewController, animated: true)
case .failure:
let alertController = UIAlertController(
title: nil,
message: NSLocalizedString("attachment.unable-to-export-media", comment: ""),
preferredStyle: .alert)
let okAction = UIAlertAction(
title: NSLocalizedString("ok", comment: ""),
style: .default) { _ in }
alertController.addAction(okAction)
self.present(alertController, animated: true)
}
}
}
}
}
} }
extension ImageViewController: UIScrollViewDelegate { extension ImageViewController: UIScrollViewDelegate {