AVPlayerLayer not sizing correctly - ios

I have a custom UITableViewCell that displays either an image or a video. If there's a video, it initially downloads and displays the thumbnail from that video while the video is loading and preparing to play.
It all works, the problem is that I get the ratio of the thumbnail, store it in the database and when I download the thumbnail, it makes the height based on that ratio. It works great for images. But the issue is that the AVPlayerLayer frame doesn't cover the entire thumbnail and it looks like this:
Here's how I do it:
func updateView() {
if let videoUrlString = post?.videoUrl, let videoUrl = URL(string: videoUrlString) {
volumeView.isHidden = false
player = AVPlayer(url: videoUrl)
playerLayer = AVPlayerLayer(player: player)
playerLayer!.frame = postImageView.frame // thumbnail image
playerLayer!.videoGravity = AVLayerVideoGravity.resizeAspectFill
contentView.layer.addSublayer(playerLayer!)
self.volumeView.layer.zPosition = 1
layoutIfNeeded()
player?.play()
player?.isMuted = videoIsMuted
}
if let ratio = post?.ratio {
photoHeightConstraint.constant = UIScreen.main.bounds.width / ratio
layoutIfNeeded()
}
}
I'm not doing something right apparently or I'm missing something based on my final result. Any clue?
UPDATE:
I notice that if you scroll the cells slowly, the video is being displayed according to the size of the thumbnail. But if you scroll fast, it mismatches the frame. I assume it has something to do with reusability or it simply cannot catch up with the frame? Here's my code for prepareForReuse() method:
override func prepareForReuse() {
super.prepareForReuse()
profileImageView.image = UIImage(named: "placeholder")
postImageView.image = UIImage(named: "profile_placeholder")
volumeView.isHidden = true
if let p = player, let pLayer = playerLayer {
p.pause()
pLayer.removeFromSuperlayer()
}
}

The solution turned out to be rather simple. Basically each post contains either post or a video, but either way, it always contains an image. I also set the ratio of that image (the width devided by the height). So, all I had to do is set the player's width to the screen width devided by the ratio and it gives your the proper frame.
playerLayer.frame.size.height = UIScreen.main.bounds.width / postRatio

Related

Swift PDFKit: Unable to scale view to a small size

I am attempting to display a pdf document on a pdfView and would like it to scale such that I can see the full content view within the bounds of the device. However, I am unable to scale it small enough to do so. I have attempted some solutions on SO but still doesn't work.
My implementation so far:
let pdfView: PDFView = {
let v = PDFView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
//addSubview and autolayout constraints to superview
if let url = Bundle.main.url(forResource: "documentName", withExtension: "pdf"), let document = PDFDocument(url: url) {
pdfView.document = document
guard let page = document.page(at: 0) else {return}
let pageSize = page.bounds(for: .cropBox)
let screenSize = UIScreen.main.bounds
let scale = screenSize.width / pageSize.width
pdfView.scaleFactor = scale
}
}
Note that these are things I have tried but doesn't work:
pdfView.autoScales = true
pdfView.displayMode = .singlePage
pdfView.minScaleFactor = // to a very small value
I also attempted to iterate the scaleFactor manually by setting 0.01 but realised that the view will not scale lower than 0.25. Is there a way for me to scale even lower? The order of scaling in my appliction is in the order of 0.1-0.2. Thanks.
If I remember correctly, PDFKit seems a bit buggy concerning autoScales. PDFView must be set up in the correct order. You can try:
Add PDFView in subview in viewDidLoad
Set up other PDFView properties, like displayMode, displayDirection, and set up constraint for PDFView in viewWillAppear
Finally, set pdfView.document = document then pdfView.autoScales = true
This setup order work for me.
I don't have answer for your next question. But it could also be related to the setup order. Let me know if it works for you.
Hope this helps.

AVPlayer get rid of black bars/background at top & bottom (Swift)

Quick question, does anybody know how i can get rid of the black bars at the top and bottom of my video? I just started using AVPlayer and i'm just removing codes here and there in attempt to remove the black layers. Appreciate those who can help me, Thanks!
UIViewController
import UIKit
import AVKit
import AVFoundation
private var looper: AVPlayerLooper?
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let path = Bundle.main.path(forResource: "Innie-Kiss", ofType:"mp4")
let player = AVQueuePlayer(url: URL(fileURLWithPath: path!))
looper = AVPlayerLooper(player: player, templateItem: AVPlayerItem(asset: AVAsset(url: URL(fileURLWithPath: path!))))
let controller = AVPlayerViewController()
controller.player = player
let screenSize = UIScreen.main.bounds.size
let videoFrame = CGRect(x: 0, y: 200, width: screenSize.width, height: (screenSize.height - 130) / 2)
controller.view.frame = videoFrame
self.view.addSubview(controller.view)
player.play()
}
}
You need to set the videoGravity of the AVLayer. The default value is .resizeAspect which will preserve the aspect ratio with black bars on top/bottom or left/right. What you want is .resize
The AVLayer you need to set the videoGravity on is the layer property of your AVPlayer.
see this for more info: https://developer.apple.com/documentation/avfoundation/avplayerlayer/1388915-videogravity
#import AVKit
var playerController = AVPlayerViewController()
playerController.videoGravity = .resizeAspect
/*playerController.videoGravity = .resizeAspectFill*/
playerController.view.backgroundColor = UIColor.white //set color as per super or custom view.
Note: If you set the videoGravity of the AVPlayerViewController. The default value is .resizeAspect then it is possible to visibility of black color(default) in background if you wants this color would similar to your super view's color then you must set superview's or custom view's color to your playercontroller's view background color.
*Example:Suppose super view's or custom view's background color is white then playerController view's background color must be set as playerController.view.backgroundColor = UIColor.white *
Note :playerController's backgroundColor should not be set as UIColor.white ,always. It must me set as super view' background color.
If you set playerController.videoGravity = .resizeAspectFill then it will fill the player content to super view or custom view, here also no black color would be shown. So choose either .resizeAspect or resizeAspectFill as per your requirements.

ios - replacing an image with a video player dynamically

My app (Swift 3, with a UIStoryboard) shows a list of episodes from a show. Whenever the user clicks on an episode from the table, he/she gets a detail view which shows (top to bottom):
the episode's image
the title
a description of the episode and technical information
Depending on the user type, instead of showing the episode's image, we would like to have a video player so people who are subscribed can just start watching the video (unsubscribed users would just see the image).
Based on the subscribed status of our users, a video player should be playing the content (subscribed) or showing an image (unsubscribed).
Is there a way to dynamically remove the image and replace it with the video player in the exact same spot and dimensions? And if so, what would happen with the Auto Layout contraints currently applied to the image?
Place a UIView on top of the UIImageView in storyboard. Align it to all 4 edges of UIImageView. And then hide and show them according to requirement by setting isHidden property.
Then, Insert the AVPlayer in the UIView through code.
Steps
The following solution works for both UIStoryboard and UI in code.
In InterfaceBuilder drag a UIView of preferred size onto the canvas, add your auto layout constraints and create an outlet (called "containerView")
Configure an AVPlayer with a URL or file, initialize an AVPlayerLayer instance with the player and add the layer to the containerView's layer hierarchy
Add an UIImageView on top of the container view
Based on your condition, show / hide / stop / play your movie and image
Quick code example
import UIKit
import AVFoundation
class ViewController: UIViewController {
// MARK: - Properties
#IBOutlet var containerView: UIView!
private var imageView: UIImageView!
private var isUserSubscribed = true
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
// Video
let url = URL(string: "https://content.jwplatform.com/manifests/vM7nH0Kl.m3u8")!
let avPlayer = AVPlayer(playerItem: AVPlayerItem(url: url))
let avPlayerLayer = AVPlayerLayer(player: avPlayer)
avPlayerLayer.frame = containerView.bounds
containerView.layer.insertSublayer(avPlayerLayer, at: 0)
// Image
imageView = UIImageView(image: UIImage(named: "someImage"))
imageView.frame = containerView.frame
containerView.addSubview(imageView)
// Condition
if isUserSubscribed {
imageView.isHidden = true
avPlayer.play()
} else {
imageView.isHidden = false
}
}
}
Result
Further options
With this approach you need to add your own playback controls, which basically boils down to adding buttons with actions, triggering AVPlayer.play(), AVPlayer.stop() etc.

Save what user see in AVCaptureVideoPreviewLayer

I'm trying to develop an app with custom camera where the user can add filters or sticker (like in TextCamera app) and share in social feed.
But I found my first problem.
I show the preview to the user with AVCaptureVideoPreviewLayer, take the photo and pass it to another view controller in a UiImageView but the second picture is bigger than first one.
I tried to resize the picture with this function:
func resize(image: UIImage) -> UIImage {
let size = image.size
let newWidth = CGFloat(size.width)
let newHeight = CGFloat(size.height - blackBottomTab.bounds.size.height)
let newSize = CGSizeMake(newWidth,newHeight)
let rect = CGRectMake(0, 0, newSize.width, newSize.height)
UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0)
image.drawInRect(rect)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}
In this function I subtract the height of the black view (under the button) from the image height. But the result that I have is different (see the photo attached).
This is my preview with a black view under the button
This is the photo taken larger than preview one
I also tried to use Aspect Fit in Storyboard Image View of the second View Controller but the result is the same.
Where is my error? Thank you to everyone that help me!
I think that the AVCaptureVideoPreviewLayer frame is the same as the screen's frame (UIScreen.mainScreen().bounds), and you added the "Shoot Photo" black view on top of it. Instead, you should change the frame of the AVCaptureVideoPreviewLayer.
Your case (what I think):
Assuming that the green rectangle is the AVCaptureVideoPreviewLayer frame and the red one is the black view frame. So, it covers (on top) of the green rectangle.
Make them look like this:
Hope that helped.
I had to solve a similar problem. As the question notes, there does not appear to be an easy way to detect the size of the video preview.
My solution is hinted at at the end of the answer to https://stackoverflow.com/a/57996039/10449843 which explains in detail how I take the snapshot and create the combined snapshot with the sticker.
Here is an elaboration of that hint.
While I use AVCapturePhotoCaptureDelegate to take the snapshot, I also use AVCaptureVideoDataOutputSampleBufferDelegate to sample the buffer once when the preview is first shown to detect the proportions of the snapshot.
// The preview layer is displayed with aspect fill
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.videoGravity = .resizeAspect
previewLayer.frame = self.cameraView.bounds
Detect the size of the preview:
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
// Only need to do this once
guard self.sampleVideoSize == true else {
return
}
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
return
}
// Sample the size of the preview layer and then store it
DispatchQueue.main.async {
let width = CGFloat(CVPixelBufferGetWidth(pixelBuffer))
let height = CGFloat(CVPixelBufferGetHeight(pixelBuffer))
// Store the video size in a local variable
// We don't care which side is which, we just need the
// picture ratio to decide how to align it on different
// screens.
self.videoSize = CGSize.init(width: width, height: height)
// Now we can set up filters and stickers etc
...
// And we don't need to sample the size again
self.sampleVideoSize = false
}
return
}

Partial-screen video preview distorted regardless of UIViewContentMode selection

I'm trying to make a simple app which will show in the top half of the iphone screen a raw preview of what the back camera sees, while in the bottom half the same preview but with various filters applied.
I first got the raw preview part working, not too hard thanks to several SO and blog posts. The UIImageView I'm displaying to takes up the entire screen for that part.
To get a half-screen view I just divide the image view's height by two, then set its contentMode to show everything while keeping the same aspect ratio:
imageView = UIImageView(frame: CGRectMake(0,0, self.view.frame.size.width, self.view.frame.size.height/2))
imageView.contentMode = UIViewContentMode.ScaleAspectFit
The height reduction works, but the image in the view is compressed vertically (e.g. a coin viewed straight-on looks like a horizontal oval). I don't think it's a coincidence that the appearance of the preview looks like the contentMode default ScaleToFill, but nothing I've tried changes the mode.
The complete code is below - the project has one scene with one view controller class; everything's done programatically.
Thanks!
import UIKit
import AVFoundation
class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate
{
var imageView : UIImageView!
override func viewDidLoad()
{
super.viewDidLoad()
imageView = UIImageView(frame: CGRectMake(0,0, self.view.frame.size.width, self.view.frame.size.height/2))
imageView.contentMode = UIViewContentMode.ScaleAspectFit
view.addSubview(imageView)
let captureSession = AVCaptureSession()
captureSession.sessionPreset = AVCaptureSessionPresetHigh
let backCamera = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
do
{
let input = try AVCaptureDeviceInput(device: backCamera)
captureSession.addInput(input)
}
catch
{
print("Camera not available")
return
}
// Unused but required for AVCaptureVideoDataOutputSampleBufferDelegate:captureOutput() events to be fired
let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
view.layer.addSublayer(previewLayer)
let videoOutput = AVCaptureVideoDataOutput()
videoOutput.setSampleBufferDelegate(self, queue: dispatch_queue_create("SampleBufferDelegate", DISPATCH_QUEUE_SERIAL))
if captureSession.canAddOutput(videoOutput)
{
captureSession.addOutput(videoOutput)
}
videoOutput.connectionWithMediaType(AVMediaTypeVideo).videoOrientation = AVCaptureVideoOrientation.Portrait
captureSession.startRunning()
}
func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!)
{
let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
let cameraImage = CIImage(CVPixelBuffer: pixelBuffer!)
dispatch_async(dispatch_get_main_queue())
{
self.imageView.image = UIImage(CIImage: cameraImage)
}
}
}
I guess this line has problem because it is in viewDidLoad() method.
Not sure it is related to your issue but I feel this is important note to you.
imageView = UIImageView(frame: CGRectMake(0,0, self.view.frame.size.width, self.view.frame.size.height/2))
You can not get correct view's size in viewDidLoad() function.
Furthermore, from iOS 10, I found controls are initially sized (0, 0, 1000, 1000) from storyboard before they are correctly laid out by iOS layout engine.
Also, the size you get in viewDidLoad() can be size of view controller in storyboard. So if you laid out controls in 4 Inch screen, it will return size of 4 inch screen in viewDidLoad() method even you run the app on iPhone6 or bigger screens.
Also, please set imageView.layer.maskToBounds property to true to prevent any out bounding of image.
One more thing is that you should place code laying out your image view appropriately when the view bounds changes (Like rotation of screen).

Resources