videoGravity resizeAspectFill doesn't work - Swift - AVFoundation - swift

I'm displaying a video inside a UIView, everything works fine and Video Layer Bounds are the same of the UIView in which it is embedded.
The problem is that the video is not displayed correctly inside the view's bounds, I can only see a part of it (precisely the center).
So I searched and found out that there's a property of AVPlayerLayer which is supposed to solve this problem: .videoGravity
I implemented it with .resizeAspectFill but it doesn't change anything.
Here is the code:
class PlaceHolderVideoView : UIView{
var player = AVPlayer()
var playerLayer = AVPlayerLayer()
let containerImageView = UIImageView(image: #imageLiteral(resourceName: "VideoContainerView"), contentMode: .scaleAspectFit)
override init(frame: CGRect) {
super.init(frame: frame)
setUpUI()
setUpPlayer()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate func setUpUI(){
clipsToBounds = true
translatesAutoresizingMaskIntoConstraints = false
addSubview(containerImageView)
containerImageView.clipsToBounds = true
containerImageView.fillSuperview()
}
fileprivate func setUpPlayer(){
let urlPathString = Bundle.main.path(forResource: "dance", ofType: "mp4")
if let videoURL = urlPathString{
let url = URL(fileURLWithPath: videoURL)
player = AVPlayer(url: url)
playerLayer = AVPlayerLayer(player: player)
playerLayer.cornerRadius = 20
playerLayer.bounds = self.containerImageView.bounds
print(self.containerImageView.bounds)
playerLayer.videoGravity = .resizeAspectFill
self.layer.masksToBounds = true
self.layer.cornerRadius = 20
self.layer.addSublayer(playerLayer)
player.play()
}
}
}

To solve this problem I needed to call layoutSublayers method like this:
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: layer)
playerLayer.frame = self.bounds
}
this method can set the bounds of the playerLayer correctly

Related

Implementing UIView in SwiftUI

I am trying to implement a video player in SwiftUI via a UIView following this tutorial: https://medium.com/flawless-app-stories/avplayer-swiftui-b87af6d0553
Unfortunately the video is not displaying, I have AVFoundation imported so I am not sure of the issue. Has anyone found a solution to this?
Here are the classes I created:
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
init(frame: CGRect, urlString: String) {
super.init(frame: frame)
guard let url = URL(string: urlString) else {
return
}
let player = AVPlayer(url: url)
player.play()
playerLayer.player = player
layer.addSublayer(playerLayer)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
}
struct VideoPlayerUIView: UIViewRepresentable {
var frame: CGRect
var urlString: String
func makeUIView(context: Context) -> some UIView {
return PlayerUIView(frame: self.frame, urlString: self.urlString)
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
Here I how I am using the implementation in SwiftUI:
var body: some View {
Text("Videos")
VideoPlayerUIView(frame: CGRect(x: 0, y: 0, width: 400, height: 300), urlString: urlString)
}
This was the output (no video)
enter image description here
You did everything right here.
The only thing that is causing error is how you are process the URL.
Try initialising the player like this instead
let player = AVPlayer(url: Bundle.main.url(forResource: urlString, withExtension: "mp4")!)
Change the extension according to the format you are using. Remember, here I am force wrapping the optional. So, if you give a wrong urlString or have a video extension other than mp4, the app will crash

Swift live preview cropper

In my application, I am trying to recreate a live preview picture cropper like in the popular application "Photomath".
Initially, I divided my code into three different classes. The Live preview view (a UIView), an Overlay View (a UIView also), and the actual Cropper View.
Currently, my thought process is to add the CropperView into the Overlay and then the Overlay into the Live Preview View. So far that's working.
I have a UIPanGesture hooked up to the CropperView in order to have it move around eventually. However, here is where the problem resides.
The inside of the cropper is clear or less dark than the surrounding overlay. I've heard that you can achieve this using a mask but have not been successful.
Here is the code for the Live Preview View
class NotationCameraView : UIView {
private var session : AVCaptureSession!
private var stillImageOutput : AVCapturePhotoOutput!
private var videoPreviewLayer : AVCaptureVideoPreviewLayer!
private var previewCropper : OverlayCropper!
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInitializer()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.commonInitializer()
}
private func commonInitializer() {
if AVCaptureDevice.authorizationStatus(for: .video) == .authorized {
self.setUpLivePreview()
self.setUpOverlayCropper()
print("Set up view")
} else {
AVCaptureDevice.requestAccess(for: .video) { (success) in
if success {
self.setUpLivePreview()
self.setUpOverlayCropper()
} else {
print("Give me your camera REEEEEEEEEEE")
}
}
}
}
/**
Sets up the live preview of the back camera
*/
private func setUpLivePreview() {
self.session = AVCaptureSession()
self.session.sessionPreset = .high
let backCamera = AVCaptureDevice.default(for: AVMediaType.video)
//MARK: Need to have an overlay view so that the user does not see the video preview frame being upscaled
do {
let input = try AVCaptureDeviceInput(device: backCamera!)
self.stillImageOutput = AVCapturePhotoOutput()
if self.session.canAddInput(input) && self.session.canAddOutput(self.stillImageOutput) {
self.session.addInput(input)
self.session.addOutput(self.stillImageOutput)
self.videoPreviewLayer = AVCaptureVideoPreviewLayer(session: self.session)
self.videoPreviewLayer.videoGravity = .resizeAspectFill
self.videoPreviewLayer.connection?.videoOrientation = .portrait
self.layer.addSublayer(videoPreviewLayer)
//Have public methods for starting or stopping the capture session
DispatchQueue.global(qos: .userInitiated).async {
self.session.startRunning()
DispatchQueue.main.async {
self.videoPreviewLayer.frame = self.bounds
}
}
}
} catch {
print("Need to allow permission to the back camera")
}
}
/**
Sets up the overlay cropper for the live preview.
*/
private func setUpOverlayCropper() {
self.previewCropper = OverlayCropper(frame: self.bounds)
self.previewCropper.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(self.previewCropper)
}
}
Here is the code for the Overlay.
class OverlayCropper : UIView {
private var cropperView : CropperView!
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInitializer()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.commonInitializer()
}
private func commonInitializer() {
self.cropperView = CropperView(frame: CGRect(x: 50, y: 100, width: 100, height: 100))
self.cropperView.backgroundColor = UIColor.blue
self.cropperView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(cropperView)
self.backgroundColor = UIColor.black.withAlphaComponent(0.25)
}
}
and lastly here is the code for the CropperView
class CropperView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInitializer()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.commonInitializer()
}
private func commonInitializer() {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.handlePanGesture(_:)))
self.addGestureRecognizer(panGesture)
self.makeMask()
}
#objc private func handlePanGesture(_ panGesture : UIPanGestureRecognizer) {
let translation = panGesture.translation(in: self)
//print(translation.x, translation.y)
self.center = CGPoint(x: self.center.x + translation.x, y: self.center.y + translation.y)
panGesture.setTranslation(CGPoint.zero, in: self)
}
private func makeMask() {
let path = CGMutablePath()
path.addRect(CGRect(origin: .zero, size: self.frame.size))
let maskLayer = CAShapeLayer()
maskLayer.backgroundColor = UIColor.clear.cgColor
maskLayer.path = path
maskLayer.fillRule = .evenOdd
self.layer.mask = maskLayer
self.clipsToBounds = true
}
}

How can I update what video is playing on AVPlayerLayer in SwiftUI?

I have a #State variable in my main ContentView called url that is the source to an mp4 file. How can I modify either PlayerView or VideoView (both below) so that when something causes url to change in ContentView, the VideoView updates itself to play the new video at url?
I feel like I am on the right track by adding a Coordinator in VideoView, but this is something I saw in Apple's tutorials, and I don't really understand how to use it.
PlayerView.swift
import UIKit
import AVKit
class PlayerView: UIView {
private let playerLayer = AVPlayerLayer()
private var playerLooper: AVPlayerLooper?
init(frame: CGRect, url: URL) {
super.init(frame: frame)
// Obtain asset and create an item from it
let asset = AVAsset(url: url)
let item = AVPlayerItem(asset: asset)
// Create the video player using the URL passed in.
let player = AVQueuePlayer()
// Add the player to our Player Layer
playerLayer.player = player
playerLayer.videoGravity = .resizeAspect // Resizes content to fill whole video layer.
playerLayer.backgroundColor = UIColor.black.cgColor
layer.addSublayer(playerLayer)
// Create new player looper
playerLooper = AVPlayerLooper(player: player, templateItem: item)
// Start the movie
player.volume = 0
player.play()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
}
VideoView
(wrapper)
import SwiftUI
import AVKit
struct VideoView: UIViewRepresentable {
#Binding var videoURL: URL
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIView {
return PlayerView(frame: .zero, url: videoURL)
}
func updateUIView(_ playerView: UIView, context: Context) {
}
class Coordinator: NSObject {
var parent: VideoView
init(_ videoView: VideoView) {
self.parent = videoView
}
}
}

How can I play a video using AVPlayer?

Apple Swift version 3.0.2 (swiftlang-800.0.63 clang-800.0.42.1)
I would like to play a video from internet using AVPlayer.
But, a error occurred "super.init(frame: frame)" in AVPlayer.swift of below source list.
Thread 1:EXC_BAD_ACCESS (code=2, address=0x16fc07fe0)
How can I play a video using AVPlayer?
If there is wrong point except point of error, let me know it, too.
AVPlayerView.swift
import Foundation
import UIKit
final class AVPlayerView : UIView {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
override init(frame: CGRect) {
super.init(frame: frame)
}
override public class var layerClass: Swift.AnyClass {
get {
return AVPlayerView.self
}
}
}
ViewController.swift
import UIKit
import AVFoundation
import CoreMedia
class ViewController: UIViewController {
var playerItem : AVPlayerItem!
var videoPlayer : AVPlayer!
override func viewDidLoad() {
super.viewDidLoad()
let url:NSURL = NSURL(string: "https://aaa.com/test.m3u8")!
let avAsset = AVURLAsset(url: url as URL, options: nil)
playerItem = AVPlayerItem(asset: avAsset)
videoPlayer = AVPlayer(playerItem: playerItem)
print(self.view.bounds)
let videoPlayerView = AVPlayerView(frame: self.view.bounds)
let layer = videoPlayerView.layer as! AVPlayerLayer
layer.videoGravity = AVLayerVideoGravityResizeAspect
layer.player = videoPlayer
self.view.layer.addSublayer(layer)
videoPlayer.play()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Here is code to play video:
let videoURL = NSURL(string: "PUT_YOUR_PROPER_URL")
let playerAV = AVPlayer(url: videoURL! as URL)
let playerLayerAV = AVPlayerLayer(player: playerAV)
playerLayerAV.frame = self.view.bounds
self.view.layer.addSublayer(playerLayerAV)
playerAV.play()
AVPlayer support below extension:
public.mpeg
public.mpeg-2-video
public.avi
public.aifc-audio
public.aac-audio
public.mpeg-4
public.au-audio
public.aiff-audio
public.mp2
public.3gpp2
public.ac3-audio
public.mp3
public.mpeg-2-transport-stream
public.3gpp
public.mpeg-4-audio
Make sure you can use supported video in AVPlayer.

NSImage in a CALayer and Liveness

I'm missing something here. I got Liveness to work in Interface Builder. I can draw the image in the NSView with NSImage.draw rect. So the image loads correctly. However when I put this inside a CALayer it doesn't show up.
Did I miss something about behaviour on NSView? CALayer? Layer Hosting? Or something else?
Here's the code of the view:
import Foundation
import AppKit
import QuartzCore
#IBDesignable public class CircularImageView: NSView {
var imageLayer: CALayer?
#IBInspectable public var edgeInset: CGFloat = 10
public var image: NSImage? {
didSet {
if let newImage = image {
imageLayer?.contents = newImage
}
}
}
// MARK: New in this class
private func prepareLayer() {
self.layer = CALayer()
self.wantsLayer = true
}
private func drawImage() {
// What am I doing wrong here?
var newImageLayer = CALayer()
newImageLayer.contentsGravity = kCAGravityResizeAspect
if let imageToSet = image {
newImageLayer.contents = imageToSet
}
let insetBounds = CGRectInset(self.bounds, edgeInset, edgeInset)
newImageLayer.frame = insetBounds
newImageLayer.backgroundColor = NSColor(calibratedWhite: 0.8, alpha: 1).CGColor
self.layer!.addSublayer(newImageLayer)
imageLayer = newImageLayer
}
private func test(){
image?.drawInRect(self.bounds)
}
// MARK: NSView stuff
public override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
prepareLayer()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
prepareLayer()
}
public override func viewWillDraw() {
super.viewWillDraw()
drawImage()
}
public override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
// Load default test image.
println("\(self): prepareForInterfaceBuilder")
let processInfo = NSProcessInfo.processInfo()
let environment = processInfo.environment
let projectSourceDirectories : AnyObject = environment["IB_PROJECT_SOURCE_DIRECTORIES"]!
let directories = projectSourceDirectories.componentsSeparatedByString(":")
if directories.count != 0 {
let firstPath = directories[0] as! String
let imagePath = firstPath.stringByAppendingPathComponent("CircularView/Bz1dSvR.jpg")
let image = NSImage(contentsOfFile: imagePath)
image!.setName("Test Image")
self.image = image
}
}
}
Thanks in advance.
Okay, guys I found the answer.
I did two things wrong in this code. I was working on a layer hosting view and I simply needed a layer-backed view. I didn't new there was a difference. And before adding an NSImage to the CALayer's contents I needed to embrace it with lockFocus() and unlockFocus().
Here's the full code that solved the issue.
import Foundation
import AppKit
#IBDesignable public class CircularImageView: NSView {
var imageLayer: CALayer?
#IBInspectable public var edgeInset: CGFloat = 10
public var image: NSImage? {
didSet {
if let newImage = image {
imageLayer?.contents = newImage
}
}
}
// MARK: New in this class
private func prepareLayer() {
// I had to remove my own created layer.
self.wantsLayer = true
}
private func drawImage() {
var newImageLayer = CALayer()
newImageLayer.contentsGravity = kCAGravityResizeAspect
if let imageToSet = image {
// I didn't lock the focus on the imageToSet.
imageToSet.lockFocus()
newImageLayer.contents = imageToSet
// I didn't unlock the focus either.
imageToSet.unlockFocus()
}
let insetBounds = CGRectInset(self.bounds, edgeInset, edgeInset)
newImageLayer.frame = insetBounds
newImageLayer.backgroundColor = NSColor(calibratedWhite: 0.8, alpha: 1).CGColor
self.layer!.addSublayer(newImageLayer)
imageLayer = newImageLayer
}
// MARK: NSView stuff
public override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
prepareLayer()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
prepareLayer()
}
public override func viewWillDraw() {
super.viewWillDraw()
drawImage()
}
public override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
// Load default test image.
println("\(self): prepareForInterfaceBuilder")
let processInfo = NSProcessInfo.processInfo()
let environment = processInfo.environment
let projectSourceDirectories : AnyObject = environment["IB_PROJECT_SOURCE_DIRECTORIES"]!
let directories = projectSourceDirectories.componentsSeparatedByString(":")
if directories.count != 0 {
let firstPath = directories[0] as! String
let imagePath = firstPath.stringByAppendingPathComponent("CircularView/Bz1dSvR.jpg")
let image = NSImage(contentsOfFile: imagePath)
image!.setName("Test Image")
self.image = image
}
}
}