Set size for Video with AVPlayerViewController - ios

I need to play a short video at the end of my game.
I created an AVPlayerViewController on my storyboard with a modal presentation using a segue from the previous View Controller.
However I want the video NOT to cover the whole screen. Let's say 50% (centered) of the screen size.
Here is the code I tried :
import AVKit
import AVFoundation
import UIKit
class VideoPlayerViewController: AVPlayerViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(animated: Bool) {
// Play video
player?.play()
}
override func viewDidLayoutSubviews() {
print(videoBounds)
self.view.bounds = videoBounds
}
}
The problem is that the video scales to the whole screen is very small (like something around 100*70px). I believe the default size is 100*100 at some point and it scales it to match my video ratio.
If I don't specify bounds, it scales to the whole screen (video gravity don't let me the choice).
If I reduce the size on my whole view in viewDidLayoutSubviews, it's still not okay because this method is called multiple times so I keep changing my view bounds.
I can't believe how hard it is to display a local video on part of the screen with AVPlayerViewController...
Any help ?

You can make the view controller any size you'd like. Like this, if you wanted to:
let videoURL = URL(fileURLWithPath: videoPath)
let player = AVPlayer(url: videoURL)
let playerViewController = AVPlayerViewController()
playerViewController.view.frame = CGRect (x:100, y:100, width:200, height:100)
playerViewController.player = player
self.addChildViewController(playerViewController)
self.view.addSubview(playerViewController.view)
playerViewController.didMove(toParentViewController: self)
(obviously, the above size would be weird, but you can. if you want to).
Use the ViewController if you want automatically managed player controls. If you want to roll your own controls, the layer is probably better.
Just posting this to correct the answer that indicated you shouldn't use the VC except in full screen. Seems to work fine for me.

AVPlayerViewController is only meant to be used in a full screen manner, you are better off using AVPlayerLayer this allows you to size / place it in a view of your choosing.
let player = AVPlayer(url: myURL)
let layer = AVPlayerLayer(player: player)
layer.frame.size = CGSize(100,100)
layer.frame.origin = myView.center
player.play()

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.

Removing the QuickTime icon from AVPlayerViewController

I am building a screen where users can play audio files using an AVPlayerViewController. The problem is that I can't get rid of the QuickTime logo in the player view, see screenshot:
My code:
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
guard let url = URL(string: filePath!) else {
return
}
let player = AVPlayer(url: url)
let controller = AVPlayerViewController()
controller.modalPresentationStyle = .overFullScreen
controller.player = player
self.present(controller, animated: true) {
player.play()
}
} catch {
}
I've tried adding an UIImageView using controller.contentOverlayView?.addSubView(), but I can't center that properly. How do I customize the layout of the player without having to build my own interface from scratch?
What you tried is correct: just add a subview to the content overlay view. But you left out one step: you must give both the subview and the content overlay view constraints, to make them cover the player controller’s view completely.
Example (my av is your controller):
let iv = UIView()
iv.backgroundColor = .white
av.contentOverlayView!.addSubview(iv)
let v = av.contentOverlayView!
iv.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
iv.bottomAnchor.constraint(equalTo:v.bottomAnchor),
iv.topAnchor.constraint(equalTo:v.topAnchor),
iv.leadingAnchor.constraint(equalTo:v.leadingAnchor),
iv.trailingAnchor.constraint(equalTo:v.trailingAnchor),
])
NSLayoutConstraint.activate([
v.bottomAnchor.constraint(equalTo:av.view.bottomAnchor),
v.topAnchor.constraint(equalTo:av.view.topAnchor),
v.leadingAnchor.constraint(equalTo:av.view.leadingAnchor),
v.trailingAnchor.constraint(equalTo:av.view.trailingAnchor),
])
If all you want to do is remove the "Q" logo without replacing it with anything, you can get rid of this way:
(avPlayerViewController?.view.subviews.first?.subviews.first as? UIImageView)?.image = nil
But, at least for me, the "Q" logo usually only appears about 2 seconds after I've set up the player view controller. So you might need to use a timer to get the above code to run about 2 seconds after creating the player.

Videos in welcome screen using AVPlayer causing memory leak?

In my app, there is a welcome screen consist of 3 screens. They all inherit from one super class. The different between them are just texts and video url:
|-SuperWelcomeScreenViewController
|----FirstWelcomeScreenViewController
|----SecondWelcomeScreenViewController
|----ThirdWelcomeScreenViewController
In the super class, I have a view to load view:
private final var player: AVPlayer = AVPlayer()
private final lazy var videoView: UIView = {
let v = UIView()
let videoString:String? = Bundle.main.path(forResource: self.videoPath, ofType: "mp4")
guard let unwrappedVideoPath = videoString else {return v}
let videoUrl = URL(fileURLWithPath: unwrappedVideoPath)
let item = AVPlayerItem(url: videoUrl)
self.player.replaceCurrentItem(with: item)
let layer: AVPlayerLayer = AVPlayerLayer(player: player)
//Using the size of the video
layer.frame = CGRect(x: -125, y: 0, width: 250, height: 541)
layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
v.layer.addSublayer(layer)
return v
}()
Here is the issue: I test the memory usage on Simulator, in the beginning it's about 250MB. Then I swiped to next welcome screen, it became 450MB. Then next for 550MB. My welcome screen has a infinite loop, but the usage stays at maximum after 3 viewcontrollers are all showed.
Then I went to the login screen, and then went back to welcome screen. The initial usage became 750MB. If I keep jumping from the login screen and the welcome screen, the usage will keep raising!
Here are what I want to do:
First, I want memory usage stay steady when I jumping from the login screen and the welcome screen.
Second, I want recycle memory between welcome screens. So that in every screen the usage would roughly be 250MB.
Here are what I have tried and failed:
Change player to static
Remove AVPlayerLayer from superLayer when view willDisappear
Set AVPlayer to nil
FYI I ran CFGetRetainCount in viewDidLoad and it's 5. I cannot find the other 4.
Actually doing things below together did the trick
Remove AVPlayerLayer from superLayer when view willDisappear
Set AVPlayer to nil

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.

Displaying artwork for audio stream in Swift and AVPlayer? [duplicate]

This question already has answers here:
AVPlayerViewController using audio-only AVPlayer
(2 answers)
Closed 6 years ago.
I am using an AVPlayerViewController to play an audio stream, on tvOS, and I am now wanting to have some associated artwork displayed at the same time.
I am not sure whether I should be somehow overlaying it on top or be telling the AVPlayerViewController about the artwork. If it is the latter I can't seem to see the right way to tell the AVPlayerViewController about it? Does anyone have indications as to an appropriate approach?
class StreamPlayerViewController: AVPlayerViewController
var playerItem:AVPlayerItem?
override func viewDidLoad() {
let videoURL = NSURL(string: "http://example.org/aac.m3u")
playerItem = AVPlayerItem(URL: videoURL!)
self.player=AVPlayer(playerItem: playerItem!)
let playerLayer=AVPlayerLayer(player: player)
playerLayer.frame=CGRectMake(0, 0, 300, 50)
self.view.layer.addSublayer(playerLayer)
// TODO add art work or other metadata, not coming from stream
// TODO Tell player stream is of infinite time
self.player?.play()
}
}
Following off the comment provided by #JAL (original code), is the following code:
override func viewWillAppear(animated: Bool) {
let image = UIImage(named: "stationAlbumArt")
let albumArtView = UIImageView(image: image)
// ensure view is screen size
albumArtView.frame = UIScreen.mainScreen().bounds
// set bounds, so that image stretch to fill
albumArtView.bounds = CGRectMake(0, 0, image!.size.width, image!.size.height)
self.contentOverlayView?.addSubview(albumArtView)
}
The two key additions here are setting the frame and the bounds. The frame here is using the screen size, since self.view.frame is not available until the viewDidAppear() is called. The bounds need to be set to ensure the image doesn't stretch to fill the screen.
One other thing, while the AVPlayerLayer needs to be instantiated it doesn't actually seem to need to be added as a layer, at least based on experimentation.
The only thing missing at this point is the ability to mask the time bar, without losing the audio control menu.

Resources