#カメラロールから取得した動画のトリミング方法
調べた記事を見ていると、
UIImagePickerControllerとUIVideoEditorControllerを用いてやる的なことが書いてあったのだけど、実はUIImagePickerControllerだけでできるとのこと、、、
(参考記事が少なすぎる💢)
とりあえずどちらでもできるようにしたので、どっちも書きます。。。
どちらも
iOS13.1.2で動作確認ずみ
UIImagePickerControllerとUIVideoEditorControllerを用いてカメラロールから取得した動画トリミング
いらない記述があるかもしれない。
参考記事 https://github.com/albertbori/iOS-Sample-Video-Trimmer
import UIKit
import Photos
import MobileCoreServices
//省略
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
//動画のとき
if #available(iOS 11.0, *) {
if let asset = info[.phAsset] as? PHAsset {
let options = PHVideoRequestOptions()
options.isNetworkAccessAllowed = true
let manager = PHImageManager.default()
manager.requestAVAsset(forVideo: asset, options: options) {asset, audioMix, info in
guard let asset = asset else {
print("asset is nil")
return
}
if let assetUrl = asset as? AVURLAsset {
let tempFolderUrl = URL(fileURLWithPath: NSTemporaryDirectory()).standardizedFileURL
let asset = AVAsset(url: assetUrl.url)
let outputUrl = tempFolderUrl.appendingPathComponent(assetUrl.url.lastPathComponent)
guard let exporter = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
print("次のビデオをエクスポートできませんでした: \(assetUrl.url)")
return
}
exporter.outputURL = outputUrl
exporter.outputFileType = AVFileType.mp4
exporter.exportAsynchronously {
DispatchQueue.main.async {
picker.dismiss(animated: true, completion: nil)
}
self.showEditor(for: outputUrl)
}
}
}
}
}
}
func mimeTypeForPath(path: String) -> String {
let url = NSURL(fileURLWithPath: path)
let pathExtension = url.pathExtension
if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension! as NSString, nil)?.takeRetainedValue() {
if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() {
return mimetype as String
}
}
return "application/octet-stream"
}
func showEditor(for outputUrl: URL) {
guard UIVideoEditorController.canEditVideo(atPath: outputUrl.path) else {
print("ビデオは編集できません: \(outputUrl.path)")
return
}
DispatchQueue.main.async {
let vc = UIVideoEditorController()
vc.videoPath = outputUrl.path
vc.videoMaximumDuration = 15
vc.videoQuality = UIImagePickerController.QualityType.typeIFrame960x540
vc.delegate = self
self.present(vc, animated: true, completion: nil)
}
}
static func deleteAsset(at path: String) {
do {
try FileManager.default.removeItem(at: URL(fileURLWithPath: path))
print("Deleted asset file at: \(path)")
} catch {
print("Failed to delete assete file at: \(path).")
print("\(error)")
}
}
//ビデオのURLからサムネイル画像を作成
func previewImageFromVideo(_ url:URL) -> UIImage? {
print("動画からサムネイルを生成(URL)")
let asset = AVAsset(url: url)
let imageGenerator = AVAssetImageGenerator(asset: asset)
imageGenerator.appliesPreferredTrackTransform = true
var time = asset.duration
time.value = min(time.value, 2)
do {
let imageRef = try imageGenerator.copyCGImage(at: time, actualTime: nil)
return UIImage(cgImage: imageRef)
} catch {
print(error) //エラーを黙って捨ててしまってはいけない
return nil
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true, completion: nil)
}
}
extension GameEditVC: UIVideoEditorControllerDelegate {
//動画トリミング成功時
func videoEditorController(_ editor: UIVideoEditorController, didSaveEditedVideoToPath editedVideoPath: String) {
//エラーで2回これが行われるので、こういった処理をする
if editorBool == false {
editorBool = true
return
} else {
editorBool = false
print("このパスに保存完了: \(editedVideoPath)")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
GameEditVC.deleteAsset(at: editor.videoPath)
}
let videoURL = URL(fileURLWithPath: editedVideoPath)
print(videoURL) //<-危険な強制アンラップは可能な限り避ける
guard let image = previewImageFromVideo(videoURL) else {
print("previewImageFromVideo(\(videoURL)) is nil")
return
}
self.profileImages.append(image)
DispatchQueue.main.async {
self.collectionView.reloadData()
}
editor.dismiss(animated:true, completion: nil)
}
}
// 動画トリミング失敗時の処理
private func videoEditorController(editor: UIVideoEditorController, didFailWithError error: NSError) {
print("an error occurred: \(error.localizedDescription)")
dismiss(animated:true)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
GameEditVC.deleteAsset(at: editor.videoPath)
}
}
// 動画トリミングキャンセル時の処理
func videoEditorControllerDidCancel(_ editor: UIVideoEditorController) {
dismiss(animated:true)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
GameEditVC.deleteAsset(at: editor.videoPath)
}
}
}
UIImagePickerControllerのみでカメラロールから取得した動画トリミング
普通にこちらがおすすめです。
参考記事:
https://qiita.com/k_kuni/items/08e66d4d8074bff2c4a3
https://stackoverflow.com/questions/40354689/swift-how-to-record-video-in-mp4-format-with-uiimagepickercontroller/40354948#40354948
またアンドロイド用に.movではなく.mp4で保存したいので、そちらもやるか、、、
そしてプラスでfirestorageに保存するところまで!!
かなりいろいろ見たけど以下でできたよ、、、(まじで疲れたwww)
完成コードはこれ
import UIKit
import FirebaseAuth
import Firebase
import FirebaseStorage
import Photos
import MobileCoreServices
import AVFoundation
//撮影完了時-------------------------------
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let url = info[UIImagePickerController.InfoKey.mediaURL] as? NSURL {
self.encodeVideo(videoURL: url as URL)
picker.dismiss(animated: true, completion: nil)
}
}
func encodeVideo(videoURL: URL){
indicator.startFullIndicator(view: view)
let avAsset = AVURLAsset(url: videoURL)
let startDate = Date()
let exportSession = AVAssetExportSession(asset: avAsset, presetName: AVAssetExportPresetPassthrough)
let docDir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let myDocPath = NSURL(fileURLWithPath: docDir).appendingPathComponent("temp.mp4")?.absoluteString
let docDir2 = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] as NSURL
let filePath = docDir2.appendingPathComponent("rendered-Video.mp4")
deleteFile(filePath!)
if FileManager.default.fileExists(atPath: myDocPath!){
do{
try FileManager.default.removeItem(atPath: myDocPath!)
}catch let error{
print(error)
indicator.stopFullIndicator(view: view)
}
}
exportSession?.outputURL = filePath
exportSession?.outputFileType = AVFileType.mp4
exportSession?.shouldOptimizeForNetworkUse = true
let start = CMTimeMakeWithSeconds(0.0, preferredTimescale: 0)
let range = CMTimeRange(start: start, duration: avAsset.duration)
exportSession?.timeRange = range
exportSession!.exportAsynchronously{() -> Void in
switch exportSession!.status{
case .failed:
print("\(exportSession!.error!)")
self.indicator.stopFullIndicator(view: self.view)
case .cancelled:
print("Export cancelled")
self.indicator.stopFullIndicator(view: self.view)
case .completed:
let endDate = Date()
let time = endDate.timeIntervalSince(startDate)
print(time)
print("Successful")
print(exportSession?.outputURL ?? "")
let profileMovieRef = self.gameUserStorageRef.child("profileImage_\(self.profileImages.count).mp4")
profileMovieRef.putFile(from: exportSession!.outputURL!, metadata: nil, completion: { (metadata, error) in
if error != nil {
print("エラー:\(error!)")
self.indicator.stopFullIndicator(view: self.view)
} else {
profileMovieRef.downloadURL { (url, error) in
guard let downloadURL = url else {
print("エラー:\(error!)")
self.indicator.stopFullIndicator(view: self.view)
return
}
self.indicator.stopFullIndicator(view: self.view)
}
}
}
)
default:
break
}
}
}
func deleteFile(_ filePath:URL) {
guard FileManager.default.fileExists(atPath: filePath.path) else{
return
}
do {
try FileManager.default.removeItem(atPath: filePath.path)
}catch{
fatalError("Unable to delete file: \(error) : \(#function).")
}
}
//ビデオのURLからサムネイル画像を作成
func previewImageFromVideo(_ url:URL) -> UIImage? {
print("動画からサムネイルを生成(URL)")
let asset = AVAsset(url: url)
let imageGenerator = AVAssetImageGenerator(asset: asset)
imageGenerator.appliesPreferredTrackTransform = true
var time = asset.duration
time.value = min(time.value, 2)
do {
let imageRef = try imageGenerator.copyCGImage(at: time, actualTime: nil)
return UIImage(cgImage: imageRef)
} catch {
print(error) //エラーを黙って捨ててしまってはいけない
return nil
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true, completion: nil)
}
func showAddAlertController() {
let alertController = UIAlertController(title: "選択してください", message: "画像・動画を追加します", preferredStyle: .actionSheet)
let albumButton = UIAlertAction(title: "画像を選択", style: .default) { (action: UIAlertAction!) in
self.indicator.startFullIndicator(view: self.view)
let sourceType:UIImagePickerController.SourceType = .photoLibrary
if UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.photoLibrary){
let cameraPicker = UIImagePickerController()
cameraPicker.sourceType = sourceType
cameraPicker.mediaTypes = ["public.image"]
cameraPicker.delegate = self
self.present(cameraPicker, animated: true, completion: nil)
self.indicator.stopFullIndicator(view: self.view)
}
}
let movieButton = UIAlertAction(title: "動画を選択", style: .default) { (action: UIAlertAction!) in
let sourceType:UIImagePickerController.SourceType = .photoLibrary
if UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.photoLibrary){
let controller = UIImagePickerController()
controller.sourceType = sourceType
controller.mediaTypes=[kUTTypeMovie as String] // 動画のみ
controller.delegate = self
controller.allowsEditing = true
controller.videoMaximumDuration = 10 // 10秒で動画を切り取る
controller.videoQuality = UIImagePickerController.QualityType.typeMedium
self.present(controller, animated: true, completion: nil)
}
}
let cancelButton = UIAlertAction(title: "キャンセル", style: .cancel, handler: nil)
alertController.addAction(albumButton)
alertController.addAction(movieButton)
alertController.addAction(cancelButton)
//alertControllerを表示させる
self.present(alertController, animated: true, completion: nil)
}