Add layer underneath subviews - ios

I am trying to add a sublayer of the function setupPlayerView below subviews which have been loaded previously. The subviews are loaded faster than the layer as the function creating the layer uses a string which is modified from another class, a UIViewController (I want to load individual videos). I found an answer on stackoverflow but the suggested solution of .insertSublayer(playerLayer, below: self.controlsContainerView.layer) did not work for me. How can I ensure the videolayer is under all my other subviews like controlsContainerView and its subviews (e.g. activityIndicatorView) in turn?
Here is what I got right now:
class PlayerView: UIView {
var urlString: String? {
didSet {
setupPlayerView()
}
}
override init(frame: CGRect){
super.init(frame: frame)
//although the function is called immediately it is not the "lower" than everything else
setupPlayerView()
//add subview with controls (e.g. spinner)
controlsContainerView.frame = frame
addSubview(controlsContainerView)
//add to subview and center spinner in subview
controlsContainerView.addSubview(activityIndicatorView) // (spinner)
controlsContainerView.addSubview(whiteView)
//...
}
//... irrelevant code (closures like `activityIndicatorView` and `whiteView`)
//setup video player
func setupPlayerView() {
if let urlString = self.urlString, let videoURL = NSURL(string: urlString){
print(urlString)
//player's video
if self.player == nil {
player = AVPlayer(url: videoURL as URL)
}
//add sub-layer
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = self.frame
self.controlsContainerView.layer.insertSublayer(playerLayer, below: self.controlsContainerView.layer)
//when are frames actually rendered (when is video loaded so one can remove spinner)
player?.addObserver(self, forKeyPath: "currentItem.loadedTimeRanges", options: .new, context: nil)
}
}
For those interested: I change urlString the following way in another class (a ViewController):
videoPlayerView.urlString = videoUrl //videoUrl being some link to a video source

Related

AVPlayer video disappears when navigating 'back' in Swift

When navigating to a subview, I have set it up so that a video plays automatically. At the bottom of the video, there is a group of links that go to related content. When clicking one of them, a new view is pushed onto the stack and a different video starts playing.
The problem happens when using the automatically generated '< Back' button to go back to the prior view (which had a different video). This original view can be operated using the player controls, but nothing shows up on the screen.
I've tried to update the CGRect frame, use onAppear to reinitialize the video player, and also followed the advice here.
So far nothing seems to work. Here is the code I am using for the actual video player (adapted from Chris Mash's website):
import SwiftUI
import AVKit
import UIKit
import AVFoundation
let playerLayer = AVPlayerLayer()
class PlayVideo: UIView {
init(frame: CGRect, url: URL) {
super.init(frame: frame)
// Create the video player using the URL passed in.
let player = AVPlayer(url: url)
player.volume = 100 // Will play audio if you don't set to zero
player.play() // Set to play once created
// Add the player to our Player Layer
playerLayer.player = player
playerLayer.videoGravity = .resizeAspectFill // Resizes content to fill whole video layer.
playerLayer.backgroundColor = UIColor.black.cgColor
layer.addSublayer(playerLayer)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
static func pauseVideo() {
playerLayer.player?.pause()
}
}
struct ViewVideo: UIViewRepresentable {
var videoURL:URL
var previewLength:Double?
func makeUIView(context: Context) -> UIView {
return PlayVideo(frame: .zero, url: videoURL)
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
This is called from the main view using:
ViewVideo(videoURL: videoURL)
The only work around I can think of is to disable the back button and force the user to go back to the main view every time. That's a terrible option and I'm hoping someone will have some helpful advice here. Thanks -
If I understand this correctly, you play a different video, when you navigate to the new view. So you create a new PlayVideo view?
Then the problem is that your playerLayer is a static property. The new view will set a new player into the playerLayer and replace the old one. Similarly, if you pause one player, both are paused. Additionally, adding the player as a sublayer to the new view will remove it from the old view.
You need the playerLayer as a local property to your view. Or at least a AVPlayerLayer for every video you want to play. Then you need a mechanism for pausing/restarting each video, when it becomes visible. For example by implementing the viewWillAppear. This method gets always called, when you navigate back to a view/ the view becomes visible.
Thank you to #dominik-105 for helping with this question. I was able to fix the problem using the suggestions that were made.
Specifically, I removed the global definition of playerLayer and instead placed it as a local variable in my main view call:
var playerLayer = AVPlayerLayer()
I then call ViewVideo with the playerLayer: playerLayer tag, and the ViewVideo then calls PlayVideo with playerLayer:AVPlayerLayer as part of the init.
Interestingly, this leads to problems with the override layout function I was using to define the size of the video box. I define the frame directly in the init now and removed the old override function code. The full code is now:
import SwiftUI
import AVKit
import UIKit
import AVFoundation
class PlayVideo: UIView {
init(frame: CGRect, url: URL, playerLayer: AVPlayerLayer, width: CGFloat, height: CGFloat) {
super.init(frame: frame)
// Create the video player using the URL passed in.
let player = AVPlayer(url: url)
player.volume = 100 // Will play audio if you don't set to zero
player.play() // Set to play once created
// Add the player to our Player Layer
playerLayer.player = player
playerLayer.videoGravity = .resizeAspectFill // Resizes content to fill whole video layer.
playerLayer.backgroundColor = UIColor.black.cgColor
playerLayer.player?.actionAtItemEnd = .pause
layer.addSublayer(playerLayer)
playerLayer.frame = CGRect(x: 0, y: 0, width: width, height: height)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
struct ViewVideo: UIViewRepresentable {
var videoURL:URL
var playerLayer: AVPlayerLayer
var width: CGFloat
var height: CGFloat
func makeUIView(context: Context) -> UIView {
return PlayVideo(frame: .zero, url: videoURL, playerLayer: playerLayer, width: width, height: height)
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
I use GeometryReader to define the size of the box, and then pass along the width and height to the struct, which passes it along to the class.

Embedding an AVPlayerViewController into a UIView

I am trying to play a video inside a UIView, but the video player always appears outside of the UIView which I am embedding the AVPlayerViewController into.
Below is what I have so far....
class ViewController: UIViewController {
let smallVideoPlayerViewController = AVPlayerViewController()
#IBOutlet weak var videoView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let myFileManager = FileManager.default
let mainBundle = Bundle.main
let resourcesPath = mainBundle.resourcePath!
guard let allItemsInTheBundle = try? myFileManager.contentsOfDirectory(atPath: resourcesPath) else {
return
}
let videoName = "test"
let videoPath = Bundle.main.path(forResource: videoName, ofType: "mp4")
let videoUrl = URL(fileURLWithPath: videoPath!)
smallVideoPlayerViewController.showsPlaybackControls = false
smallVideoPlayerViewController.player = AVPlayer(url: videoUrl)
videoView.addSubview(smallVideoPlayerViewController.view)
smallVideoPlayerViewController.view.frame = videoView.frame
smallVideoPlayerViewController.player?.play()
}
...
}
The background colour of UIView is set to white. As seen from the screenshot, AVPlayer is outside of the UIView.
I tried manually setting the dimensions and position of the AVPlayer, but no luck with that either.
I am using Xcode 9.2. The project has not warnings regarding to any layout issues.
How can I perfectly align the AVPlayer, so that is would appear inside the UIView.
Thanks
Change this line:
smallVideoPlayerViewController.view.frame = videoView.frame
to this:
smallVideoPlayerViewController.view.frame = videoView.bounds
The reason is because videoView.frame includes the origin in its superview which you don't want. videoView.bounds is just the size with an origin of 0,0.
Of course you might want to go further set the auto-resizing mask to keep the video player frame the same like this:
smallVideoPlayerViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
or even to go full blown auto-layout.

How to reuse UICollectionViewCell with nested UIView for video

I am using a UICollectionViewController to display cells with content loaded from an external JSON api. To be specific, these cells include UILabels for the user that posted the video, the title of the video, etc.
In addition to the text labels, there is a large UIView in the middle of the cell to play video.
The following is the code for my UICollectionViewCell.
class PostCell : UICollectionViewCell {
var videoCell: VideoPlayerView
let storyTitleLabel: UILabel = {
let storyTitleLabel = UILabel()
storyTitleLabel.translatesAutoresizingMaskIntoConstraints = false
return storyTitleLabel
}()
let usernameLabel: UILabel = {
let usernameLabel = UILabel()
usernameLabel.translatesAutoresizingMaskIntoConstraints = false
return usernameLabel
}()
func prepare(post: ApiPost) {
self.usernameLabel.text = post.user?.username
self.postTitleLabel.text = post.title!
self.storyTitleLabel.text = post.story?.title
self.videoCell.prepare(post: post)
}
func focus() {
self.videoCell.focus()
}
func unfocus() {
self.videoCell.unfocus()
}
}
When a user is scrolling through the UICollectionView and the middle of the screen is over the visible UICollectionViewCell, the focus method is called.
This loads up the video corresponding to the ApiPost (api data wrapper) for that UICollectionViewCell.
I have also programmed my own subclass of UIView for the videoCell as you can see. The following is the source code of that class.
class VideoPlayerView: UIView {
func prepare(post: ApiPost) {
self.post = post
self.videoUrl = sharedConfig.apiUrl + "/stream/" + post.video!.uuid! + ".mp4"
if let url = URL(string: self.videoUrl!) {
let avPlayerItem = AVPlayerItem(url: url)
if (self.hasReceivedApiData) {
DispatchQueue.global(qos: .background).async {
print("inserting...")
self.player?.replaceCurrentItem(with: avPlayerItem)
print("done")
}
} else {
self.hasReceivedApiData = true
self.player = AVQueuePlayer.init()
self.player?.automaticallyWaitsToMinimizeStalling = false
self.player?.insert(avPlayerItem, after: nil)
self.player?.volume = 1
let playerLayer = AVPlayerLayer(player: self.player)
playerLayer.frame = CGRect(x: 0, y: -70, width: self.frame.width - 32, height: self.frame.height)
self.layer.addSublayer(playerLayer)
}
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: self.player?.currentItem, queue: nil, using: { (_) in
DispatchQueue.main.async {
self.player?.seek(to: kCMTimeZero)
self.player?.play()
}
})
}
}
func focus() {
self.player?.volume = 1.0
self.player?.play()
}
func unfocus() {
self.player?.seek(to: kCMTimeZero)
self.player?.pause()
}
}
The problem that I am having is that when I scroll over the UICollectionView, it lags really badly when prepare() is called on a reused UICollectionViewCell. It seems that calling self.player?.replaceCurrentItem(with: avPlayerItem) is slowing the UI down on scroll. I have verified this by putting the print calls between. It seems this is a slow operation that is lagging the UI, as you can see, I even tried putting that operation on a different thread, but no luck.
Am I displaying video properly in a UICollectionViewCell? Should I be doing this using some other method? The only way I could figure out to clear the old video is by using an AVQueuePlayer to clear out old AVPlayerItems, and insert the new AVPlayerItem when the cell is reused and prepared for the next video.
Any help on this would be greatly appreciated! Thanks in advance.

AVPlayer not full screen in landscape mode using iOS swift

I have a video view set to full screen. However while playing in the simulator, it is not running full screen.
This issue is only for iPads and not iPhones.
Here is my code:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
let bundle: Bundle = Bundle.main
let videoPlayer: String = bundle.path(forResource: "normalnewer", ofType: "mp4")!
let movieUrl : NSURL = NSURL.fileURL(withPath: videoPlayer) as NSURL
// var fileURL = NSURL(fileURLWithPath: "/Users/Mantas/Desktop/123/123/video-1453562323.mp4.mp4")
playerView = AVPlayer(url: movieUrl as URL)
NotificationCenter.default.addObserver(self,selector: #selector(playerItemDidReachEnd),name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,object: self.playerView.currentItem) // Add observer
var playerLayer=AVPlayerLayer(player: playerView)
// self.avPlayerLayer.playerLayer = AVLayerVideoGravityResizeAspectFill
playerLayer.frame = viewVideo.bounds
// self.playerLayer.frame = self.videoPreviewLayer.bounds
viewVideo.layer.addSublayer(playerLayer)
playerView.play()
UserDefaults.standard.set("normalnewer", forKey: "video")
}
I have tried checking landscape mode in target's general.
Gone through the threads below:
how to play a video in fullscreen mode using swift ios?
Play video fullscreen in landscape mode, when my entire application is in lock in portrait mode
Setting device orientation in Swift iOS
But these did not resolve my issue.
Here is a screenshot.
Can somebody help me resolve this?
I see you solved your issue, but I noticed you were using an AVPlayerLayer. The orientation rotation is handled with the AVPlayerViewController, but not with a custom view using a player layer. It is useful to be able to put the layer in fullscreen without rotating the device. I answered this question elsewhere, but I will put it here as well.
Transforms and frame manipulation can solve this issue:
extension CGAffineTransform {
static let ninetyDegreeRotation = CGAffineTransform(rotationAngle: CGFloat(M_PI / 2))
}
extension AVPlayerLayer {
var fullScreenAnimationDuration: TimeInterval {
return 0.15
}
func minimizeToFrame(_ frame: CGRect) {
UIView.animate(withDuration: fullScreenAnimationDuration) {
self.setAffineTransform(.identity)
self.frame = frame
}
}
func goFullscreen() {
UIView.animate(withDuration: fullScreenAnimationDuration) {
self.setAffineTransform(.ninetyDegreeRotation)
self.frame = UIScreen.main.bounds
}
}
}
Setting the frame of the AVPlayerLayer changes it's parent's frame. Save the original frame in your view subclass, to minimize the AVPLayerLayer back to where it was. This allows for autolayout.
IMPORTANT - This only works if the player is in the center of your view subclass.
Incomplete example:
class AVPlayerView: UIView {
fileprivate var avPlayerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
fileprivate var hasGoneFullScreen = false
fileprivate var isPlaying = false
fileprivate var originalFrame = CGRect.zero
func togglePlayback() {
if !hasGoneFullScreen {
originalFrame = frame
hasGoneFullScreen = true
}
isPlaying = !isPlaying
if isPlaying {
avPlayerLayer.goFullscreen()
avPlayerLayer.player?.play()
} else {
avPlayerLayer.player?.pause()
avPlayerLayer.minimizeToFrame(originalFrame)
}
}
}
After spending some time taking a good look at the Storyboard, I tried changing the fill to Aspect fill & fit and that resolved the problem :)
The best thing you can do is put this code
In viewDidLayoutSubviews and enjoy it!
DispatchQueue.main.async {
self.playerLayer?.frame = self.videoPlayerView.bounds
}

Place AVPlayerViewController inside UIView

Currently I am having a dialogue view with four controls at the bottom. Each control is loading a different view inside the dialogue. One of these controls is setting an AVPlayer inside the dialogue view and is playing it. Unfortunately the AVPlayer itself comes without playback controls.
The AVPlayerViewController how ever does have playback controls. Is it possible to place a AVPlayerViewController inside a UIView so that it does not get started as a new screen? I would like to place it inside my UIView so that everything is taking place inside my dialgoue.
You can initialize the AVPlayerViewController and add it as a child to your ViewController , and insert it as a subview.
[self addChildViewController:yourPlayerViewController];
[self.view addSubview:yourPlayerViewController.view];
[yourPlayerViewController didMoveToParentViewController:self];
And to remove it:
[yourPlayerViewController willMoveToParentViewController:nil];
[yourPlayerViewController.view removeFromSuperview];
[yourPlayerViewController removeFromParentViewController];
EDIT SWIFT METHOD:
Check here for Swift
I'm using Swift 3.2 / Xcode 9.2 / iOS 11
This is how to do it in 9 easy steps. The steps are explained above each line
**Make sure you import AVKit to have access to the AVPlayerViewController or you won't be able to use it
// 1. to have access to the AVPlayerViewController import AVKit
import AVKit
// 2. create your class properties
var videoUrl: URL?
var player: AVPlayer?
let avPVC = AVPlayerViewController()
override func viewDidLoad() {
super.viewDidLoad()
// 3. make sure the url your using isn't nil
guard let videoUrl = self.videoUrl else { return }
// 4. init an AVPlayer object with the url then set the class property player with the AVPlayer object
self.player = AVPlayer(url: videoUrl)
// 5. set the class property player to the AVPlayerViewController's player
avPVC.player = self.player
// 6. set the the parent vc's bounds to the AVPlayerViewController's frame
avPVC.view.frame = self.view.bounds
// 7. the parent vc has a method on it: addChildViewController() that takes the child you want to add to it (in this case the AVPlayerViewController) as an argument
self.addChildViewController(avPVC)
// 8. add the AVPlayerViewController's view as a subview to the parent vc
self.view.addSubview(avPVC.view)
// 9. on AVPlayerViewController call didMove(toParentViewController:) and pass the parent vc as an argument to move it inside parent
avPVC.didMove(toParentViewController: self)
}
SWIFT 4 - Subview Method
The initial method works alright, but I feel like the view controller method seems messy when I really just want a subview. So, most of the time I play the video in a custom view (rather than using the entire AVPlayerViewController).
I use this method to show the video in a subview which completely fills an existing view. In the example below, urlToVideo is the URL to the video that you want to play, and existingView is the view we want to place our new view into (as a subview).
First, create a VideoPlayerView class in a new VideoPlayerView.swift file, with this, which is directly copy/paste from Apple's website, except that I called it VideoPlayerView rather than PlayerView.
import Foundation
import UIKit
import AVFoundation
/// A simple `UIView` subclass that is backed by an `AVPlayerLayer` layer.
class VideoPlayerView: UIView {
var player: AVPlayer? {
get {
return playerLayer.player
}
set {
playerLayer.player = newValue
}
}
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
override class var layerClass: AnyClass {
return AVPlayerLayer.self
}
}
Then, when I want to play the video, I create the subview and play it:
let player = AVPlayer(url: urlToVideo)
let playerFrame = CGRect(x: 0, y: 0, width: existingView.frame.width, height: existingView.frame.height)
let videoPlayerView = VideoPlayerView(frame: playerFrame)
videoPlayerView.player = player
existingView.addSubview(videoPlayerView)
player.play()
My solution is to have your view like a canvas and basically draw the AVPlayerViewController on top of it:
#IBOutlet weak var layerVideoView: UIImageView!
guard let url = Bundle.main.path(forResource: "santana", ofType: "mp4") else { return debugPrint("video not found") }
let path = URL(fileURLWithPath: url)
let player = AVPlayer(url: path)
let playerController = AVPlayerViewController()
playerController.player = player
playerController.view.frame = CGRect(x: layerVideoView.frame.origin.x, y: layerVideoView.frame.origin.y, width: layerVideoView.frame.width, height: layerVideoView.frame.height)
self.addChild(playerController)
self.view.addSubview(playerController.view)
LayerVideoView is an image view, but you can use just a UIView if that suits.
Its is important to add constraints in the Storyboard to your View because your Video Player will mimic the shape of your View.

Resources