Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Example/OSAT-VideoCompositor.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; };
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; };
607FACEC1AFB9204008FA782 /* ViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* ViewControllerTests.swift */; };
840E1E2C2A0280FE000CC15E /* portrait.MOV in Resources */ = {isa = PBXBuildFile; fileRef = 840E1E2A2A0280FE000CC15E /* portrait.MOV */; };
840E1E2D2A0280FE000CC15E /* landscape.MOV in Resources */ = {isa = PBXBuildFile; fileRef = 840E1E2B2A0280FE000CC15E /* landscape.MOV */; };
93A7E1A5FA0310303146425B /* Pods_OSAT_VideoCompositor_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98C18AA0D7D9B8CAE531EADA /* Pods_OSAT_VideoCompositor_Tests.framework */; };
E5A5504C9043983681BB2997 /* Pods_OSAT_VideoCompositor_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B0896B476C4F194D82A4048C /* Pods_OSAT_VideoCompositor_Example.framework */; };
F96A6FA9294DC796005B0365 /* videoplayback.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = F96A6FA8294DC796005B0365 /* videoplayback.mp4 */; };
Expand Down Expand Up @@ -46,6 +48,8 @@
607FACEB1AFB9204008FA782 /* ViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerTests.swift; sourceTree = "<group>"; };
69F6BD00C34281A34B25B004 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
7BB165D49711770CC07EE07F /* Pods-OSAT-VideoCompositor_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OSAT-VideoCompositor_Example.release.xcconfig"; path = "Target Support Files/Pods-OSAT-VideoCompositor_Example/Pods-OSAT-VideoCompositor_Example.release.xcconfig"; sourceTree = "<group>"; };
840E1E2A2A0280FE000CC15E /* portrait.MOV */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = portrait.MOV; sourceTree = "<group>"; };
840E1E2B2A0280FE000CC15E /* landscape.MOV */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = landscape.MOV; sourceTree = "<group>"; };
87D796BFA4EF030BF62B4C29 /* Pods-OSAT-VideoCompositor_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OSAT-VideoCompositor_Tests.release.xcconfig"; path = "Target Support Files/Pods-OSAT-VideoCompositor_Tests/Pods-OSAT-VideoCompositor_Tests.release.xcconfig"; sourceTree = "<group>"; };
98C18AA0D7D9B8CAE531EADA /* Pods_OSAT_VideoCompositor_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_OSAT_VideoCompositor_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
AB46A411940C8C9F5CA2983A /* Pods-OSAT-VideoCompositor_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OSAT-VideoCompositor_Example.debug.xcconfig"; path = "Target Support Files/Pods-OSAT-VideoCompositor_Example/Pods-OSAT-VideoCompositor_Example.debug.xcconfig"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -108,6 +112,8 @@
F9DCDD202976D4A8002F57A0 /* AVPlayerLayer.swift */,
607FACD71AFB9204008FA782 /* ViewController.swift */,
F96A6FA8294DC796005B0365 /* videoplayback.mp4 */,
840E1E2B2A0280FE000CC15E /* landscape.MOV */,
840E1E2A2A0280FE000CC15E /* portrait.MOV */,
607FACD91AFB9204008FA782 /* Main.storyboard */,
607FACDC1AFB9204008FA782 /* Images.xcassets */,
607FACDE1AFB9204008FA782 /* LaunchScreen.xib */,
Expand Down Expand Up @@ -274,6 +280,8 @@
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */,
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */,
F96A6FA9294DC796005B0365 /* videoplayback.mp4 in Resources */,
840E1E2D2A0280FE000CC15E /* landscape.MOV in Resources */,
840E1E2C2A0280FE000CC15E /* portrait.MOV in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
98 changes: 79 additions & 19 deletions Example/OSAT-VideoCompositor/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import UIKit

class ViewController: UIViewController {
private struct Constants {
static let playButton = "play"
static let pauseButton = "pause"
static let playButton = "play.circle.fill"
static let pauseButton = "pause.circle.fill"
static let iconSize: CGFloat = 40
}

Expand Down Expand Up @@ -84,22 +84,23 @@ class ViewController: UIViewController {
let url = Bundle.main.url(forResource: "videoplayback", withExtension: "mp4")!
videoPlayerLayer = AVPlayerView(frame: .zero)
originalVideoUrl = url
view.backgroundColor = .black
view.backgroundColor = .systemBackground

videoPlayerLayer?.set(url: url)
videoPlayerLayer?.delegate = self
videoPlayerLayer?.registerTimeIntervalForObservingPlayer(1)
videoPlayerLayer.translatesAutoresizingMaskIntoConstraints = false

navigationItem.title = "OSAT Video Compositer"
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: nil)

navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), style: .plain, target: self, action: nil)
navigationItem.rightBarButtonItem?.menu = createVideoImageMenu()

navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: nil)
navigationItem.leftBarButtonItem?.menu = createWaterMarkMenu()
navigationController?.navigationBar.barStyle = .default

videoPlayerLayer.backgroundColor = .systemGray
videoPlayerLayer.play()
videoPlayerLayer.backgroundColor = .systemGroupedBackground
addSubviews()
setButtonProperties()
setupConstraints()
Expand All @@ -125,10 +126,9 @@ class ViewController: UIViewController {
playerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
playerView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
playerView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
playerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -300),

videoPlayerLayer.centerXAnchor.constraint(equalTo: playerView.centerXAnchor),
videoPlayerLayer.centerXAnchor.constraint(equalTo: playerView.centerXAnchor),
videoPlayerLayer.centerYAnchor.constraint(equalTo: playerView.centerYAnchor),
videoPlayerLayer.leadingAnchor.constraint(equalTo: playerView.leadingAnchor),
videoPlayerLayer.trailingAnchor.constraint(equalTo: playerView.trailingAnchor),
videoPlayerLayer.heightAnchor.constraint(equalTo: videoPlayerLayer.widthAnchor, multiplier: 1),
Expand All @@ -137,17 +137,20 @@ class ViewController: UIViewController {
spinner.centerXAnchor.constraint(equalTo: playerView.centerXAnchor),

sliderParentView.topAnchor.constraint(equalTo: playerView.bottomAnchor, constant: 10),
sliderParentView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -60),
sliderParentView.leadingAnchor.constraint(equalTo: videoPlayerLayer.leadingAnchor),
sliderParentView.trailingAnchor.constraint(equalTo: videoPlayerLayer.trailingAnchor),
sliderParentView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0),
sliderParentView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
sliderParentView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
sliderParentView.heightAnchor.constraint(equalToConstant: 100.0),

slider.leadingAnchor.constraint(equalTo: sliderParentView.leadingAnchor, constant: 10),
slider.trailingAnchor.constraint(equalTo: sliderParentView.trailingAnchor, constant: -10),
slider.heightAnchor.constraint(equalTo: sliderParentView.heightAnchor),

playbutton.topAnchor.constraint(equalTo: sliderParentView.topAnchor, constant: 20),
playbutton.centerYAnchor.constraint(equalTo: sliderParentView.centerYAnchor),
playbutton.leadingAnchor.constraint(equalTo: sliderParentView.safeAreaLayoutGuide.leadingAnchor),
playbutton.heightAnchor.constraint(equalToConstant: Constants.iconSize),
playbutton.widthAnchor.constraint(equalToConstant: Constants.iconSize),

slider.centerYAnchor.constraint(equalTo: sliderParentView.centerYAnchor),
slider.leadingAnchor.constraint(equalTo: playbutton.safeAreaLayoutGuide.trailingAnchor, constant: 10),
slider.trailingAnchor.constraint(equalTo: sliderParentView.safeAreaLayoutGuide.trailingAnchor, constant: -10)

])
}

Expand Down Expand Up @@ -198,6 +201,10 @@ class ViewController: UIViewController {
self.showImagePicker()
}

let multiVideo = UIAction(title: "Merge & Trim", image: nil, identifier: UIAction.Identifier("leftBtm1"), attributes: [], state: .off) { action in
self.mergeTrimVideoExample()
}

let selectImage = UIAction(title: "Select an Image", image: UIImage(systemName: "photo"), attributes: [], state: .off) { action in
self.showImagePickerForWaterMark()
}
Expand All @@ -218,14 +225,19 @@ class ViewController: UIViewController {
self.handleExportButtonAction()
}

let deferredMenu = UIDeferredMenuElement { (menuElements) in
let deferredMenu2 = UIDeferredMenuElement { (menuElements) in
let menu = UIMenu(title: "Image/Font Color", options: .displayInline, children: [addTextItem, selectImage, pickFontColor, addOnlyImageItem, setExportUrlItem])
menuElements([menu])
}

let deferredMenu1 = UIDeferredMenuElement { (menuElements) in
let menu = UIMenu(title: "Feature Example", options: .displayInline, children: [multiVideo])
menuElements([menu])
}

let elements: [UIAction] = [selectVideo]
var menu = UIMenu(title: "Select Video", children: elements)
menu = menu.replacingChildren([selectVideo, deferredMenu])
menu = menu.replacingChildren([selectVideo, deferredMenu1, deferredMenu2])
return menu
}

Expand Down Expand Up @@ -264,8 +276,8 @@ class ViewController: UIViewController {
}

private func setButtonProperties() {
playbutton.setImage(UIImage(systemName: Constants.playButton, withConfiguration: UIImage.SymbolConfiguration(pointSize: Constants.iconSize)), for: .selected)
playbutton.setImage(UIImage(systemName: Constants.pauseButton, withConfiguration: UIImage.SymbolConfiguration(pointSize: Constants.iconSize)), for: .normal)
playbutton.setImage(UIImage(systemName: Constants.playButton, withConfiguration: UIImage.SymbolConfiguration(pointSize: Constants.iconSize)), for: .selected)
}

@objc private func handlePlayButtonAction(_ sender: Any) {
Expand Down Expand Up @@ -365,6 +377,54 @@ class ViewController: UIViewController {
self.present(alertController, animated: true, completion: nil)
}

private func mergeTrimVideoExample() {
guard let portraitURL = Bundle.main.url(forResource: "portrait", withExtension: "MOV"),
let landscapeURL = Bundle.main.url(forResource: "landscape", withExtension: "MOV")
else { return }

guard let exportUrl = exportUrl else {
showExportUrlNotPresent()
return
}

videoPlayerLayer.isHidden = true
videoPlayerLayer.pause()

spinner.isHidden = false
spinner.startAnimating()

let portraitAsset = OSATVideoSource(videoURL: portraitURL, startTime: 2, duration: 5)
let landscapeAsset = OSATVideoSource(videoURL: landscapeURL, startTime: 2, duration: 5)

DispatchQueue.global().async {
let compositor = OSATVideoComposition()
compositor.makeMultiVideoComposition(from: [portraitAsset, landscapeAsset], exportURL: exportUrl) { [weak self ] session in
guard let self = self else { return }
switch session.status {
case .completed:
guard let sessionOutputUrl = session.outputURL, NSData(contentsOf: sessionOutputUrl) != nil else { return }
DispatchQueue.main.async {
self.videoPlayerLayer.set(url: sessionOutputUrl)
self.play()
self.videoPlayerLayer.isHidden = false
self.spinner.isHidden = true
self.spinner.stopAnimating()
Task {
await self.getDuration()
}
}

case .failed:
NSLog("error: \(String(describing: session.error))", "")

default: break
}
} errorHandler: { error in
NSLog("\(error)", "")
}
}
}

private func addWaterMark() {
guard let inputURL = originalVideoUrl else { return }

Expand Down
Binary file added Example/OSAT-VideoCompositor/landscape.MOV
Binary file not shown.
Binary file added Example/OSAT-VideoCompositor/portrait.MOV
Binary file not shown.
76 changes: 76 additions & 0 deletions OSAT-VideoCompositor/Classes/OSATExtension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//
// OSATExtension.swift
// OSAT-VideoCompositor
//
// Created by Urmit Chauhan on 03/05/23.
//

import AVFoundation

extension Double {
func toCMTime() -> CMTime {
return CMTime(seconds: self, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
}
}

extension CGAffineTransform {
var orientation: (orientation: UIImage.Orientation, isPortrait: Bool) {
var assetOrientation = UIImage.Orientation.up
var isPortrait = false
switch [a, b, c, d] {
case [0.0, 1.0, -1.0, 0.0]:
assetOrientation = .right
isPortrait = true

case [0.0, -1.0, 1.0, 0.0]:
assetOrientation = .left
isPortrait = true

case [1.0, 0.0, 0.0, 1.0]:
assetOrientation = .up

case [-1.0, 0.0, 0.0, -1.0]:
assetOrientation = .down

default:
break
}

return (assetOrientation, isPortrait)
}
}

extension AVAssetTrack {
var fixedPreferredTransform: CGAffineTransform {
var newT = preferredTransform
switch [newT.a, newT.b, newT.c, newT.d] {
case [1, 0, 0, 1]:
newT.tx = 0
newT.ty = 0
case [1, 0, 0, -1]:
newT.tx = 0
newT.ty = naturalSize.height
case [-1, 0, 0, 1]:
newT.tx = naturalSize.width
newT.ty = 0
case [-1, 0, 0, -1]:
newT.tx = naturalSize.width
newT.ty = naturalSize.height
case [0, -1, 1, 0]:
newT.tx = 0
newT.ty = naturalSize.width
case [0, 1, -1, 0]:
newT.tx = naturalSize.height
newT.ty = 0
case [0, 1, 1, 0]:
newT.tx = 0
newT.ty = 0
case [0, -1, -1, 0]:
newT.tx = naturalSize.height
newT.ty = naturalSize.width
default:
break
}
return newT
}
}
Loading