AVFoundation capturing video with custom resolution - objective-c

I'm writing application on OS X, which will capture frames from camera.
Is it possible to set capture setting using AVCaptureDevice.activeFormat property? I had tried this, but it didn't work (session preset overrides it).
I found that on IOS it is possible with setting SessionPreset in AVCaptureSession to AVCaptureSessionPresetInputPriority.
The main purpose is to choose more detailed video resolutions than presets.

Updated: April 08, 2020.
In macOS (unlike iOS), a capture session can automatically configure the capture format after you make changes. To prevent automatic changes to the capture format use lockForConfiguration() method. Then call the beginConfiguration() method, set properties (choose one preset out of a dozen, for instance AVCaptureSessionPresetiFrame960x540) and after that call the commitConfiguration() method. In the end you need to put unlockForConfiguration() after changing a device properties.
Or follow these steps:
Call lockForConfiguration() to acquire access to the device’s config properties.
Change the device’s activeFormat property (as mentioned above & below).
Begin capture with the session’s startRunning() method.
Unlock the device with the unlockForConfiguration().
startRunning() and stopRunning() methods must be invoked to start and stop the flow of your data from the inputs to the outputs, respectively.
You must also call lockForConfiguration() before calling the AVCaptureSession method startRunning(), or the session's preset will override the selected active format on the capture device.
However, you might hold onto a lock, without releasing that lock, if you require the device properties to remain unchanged.
Here are details in developer's documentation lockForConfiguration().
If you attempt to set the active format to one not present in the accessible formats, will throw an invalidArgumentException.
Also, there's an explanation how to change properties: macOS AVFoundation Video Capture
In AVCaptureDevice there are two properties. formats and activeFormat. format will return an NSArrary of AVCaptureDeviceFormat with contains all formats exposed by cam. You select any one format from this list and set it to activeFormat. Make sure that you set the format after you receive the exclusive access to the devlce by calling AVCaptureDevice lockForConfigration. After you set the format release the lock with AVCaptureDevice unlockForConfigration. Then start the AVCaptureSession which will give you the video frames of the format you set.
AVCaptureFormat is a wraper for CMFormatDescription. CMVideoFotmatDescription is the concreete subclass of CMFormatDescription. Use CMVideoFormatDescriptionGetDimentions() to get the width and height in the set format. Use CMFormatDescriptionGetMediaSubType() to get the video codec. For raw fotmats video codec mostly is yuvs or vuy2. For compressed formats its h264, dmb1(mjpeg) and many more.
Here's a macOS code snippet written in Swift:
import Cocoa
import AVFoundation
class ViewController: NSViewController,
AVCaptureVideoDataOutputSampleBufferDelegate {
override func viewDidAppear() {
super.viewDidAppear()
setupCameraSession()
view.layer?.addSublayer(previewLayer)
cameraSession.startRunning()
}
lazy var cameraSession: AVCaptureSession = {
let session = AVCaptureSession()
session.sessionPreset = AVCaptureSession.Preset.hd1280x720
return session
}()
lazy var previewLayer: AVCaptureVideoPreviewLayer = {
let preview = AVCaptureVideoPreviewLayer(session: self.cameraSession)
preview.bounds = CGRect(x: 0,
y: 0,
width: self.view.bounds.width,
height: self.view.bounds.height)
preview.position = CGPoint(x: self.view.bounds.midX,
y: self.view.bounds.midY)
preview.videoGravity = AVLayerVideoGravity.resize
return preview
}()
func setupCameraSession() {
let captureDevice = AVCaptureDevice.default(for: AVMediaType.video)
do {
let deviceInput = try AVCaptureDeviceInput(device: captureDevice!)
guard let camera = AVCaptureDevice.default(for: .video)
else { return }
// acquire exclusive access to the device’s properties
try camera.lockForConfiguration()
cameraSession.beginConfiguration()
camera.focusMode = .continuousAutoFocus
camera.flashMode = .on
camera.whiteBalanceMode = .continuousAutoWhiteBalance
if (cameraSession.canAddInput(deviceInput) == true) {
cameraSession.addInput(deviceInput)
}
let dataOutput = AVCaptureVideoDataOutput()
dataOutput.videoSettings = [(kCVPixelBufferPixelFormatTypeKey as NSString) :
NSNumber(value: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange as UInt32)] as [String : Any]
dataOutput.alwaysDiscardsLateVideoFrames = true
if (cameraSession.canAddOutput(dataOutput) == true) {
cameraSession.addOutput(dataOutput)
}
let preset: AVCaptureSession.Preset = .hd4K3840x2160
cameraSession.sessionPreset = preset
cameraSession.commitConfiguration()
camera.unlockForConfiguration()
let queue = DispatchQueue(label: "blah.blah.blah")
dataOutput.setSampleBufferDelegate(self, queue: queue)
} catch let error as NSError {
NSLog("\(error.localizedDescription)")
}
}
}
And here's a code snippet written in Objective-C setting min and max fps:
myCamera = NULL;
if ( NULL != myCamera ) {
if ( [ myCamera lockForConfiguration: NULL ] ) {
[ myCamera setActiveVideoMinFrameDuration: CMTimeMake( 1, 12 ) ];
[ myCamera setActiveVideoMaxFrameDuration: CMTimeMake( 1, 25 ) ];
[ myCamera unlockForConfiguration ];
}
}
return ( NULL != myCamera );

Related

Audio session .ended isn't called with two AVPlayers

Here are steps to reproduce:
Activate AVAudioSession with .playback category.
Register for AVAudioSession.interruptionNotification
Create two AVPlayers and start them
Interrupt playback by calling Siri/receiving a call by Skype, Cellular and etc.
Expected behavior:
Receiving notification of the audio session interruption with .began state at the start and .ended at the end. Also, as a side effect, Siri doesn't respond to commands.
Real behavior:
Only .began notification is called.
To bring back .ended notification (which is used to continue playback) remove one player.
Question: how to handle the audio session interruption with more than 1 AVPlayer running?
Here I created a simple demo project: https://github.com/denis-obukhov/AVAudioSessionBug
Tested on iOS 14.4
import UIKit
import AVFoundation
class ViewController: UIViewController {
private let player1: AVPlayer? = {
$0.volume = 0.5
return $0
}(AVPlayer())
private let player2: AVPlayer? = {
$0.volume = 0.5
return $0 // return nil for any player to bring back .ended interruption notification
}(AVPlayer())
override func viewDidLoad() {
super.viewDidLoad()
registerObservers()
startAudioSession()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
player1?.replaceCurrentItem(with: makePlayerItem(named: "music1"))
player2?.replaceCurrentItem(with: makePlayerItem(named: "music2"))
[player1, player2].forEach { $0?.play() }
}
private func makePlayerItem(named name: String) -> AVPlayerItem {
let fileURL = Bundle.main.url(
forResource: name,
withExtension: "mp3"
)!
return AVPlayerItem(url: fileURL)
}
private func registerObservers() {
NotificationCenter.default.addObserver(
self, selector: #selector(handleInterruption(_:)),
name: AVAudioSession.interruptionNotification,
object: nil
)
}
private func startAudioSession() {
try? AVAudioSession.sharedInstance().setCategory(.playback)
try? AVAudioSession.sharedInstance().setActive(true)
}
#objc private func handleInterruption(_ notification: Notification) {
print("GOT INTERRUPTION")
guard
let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue)
else {
return
}
switch type {
case .began:
print("Interruption BEGAN")
[player1, player2].forEach { $0?.pause() }
case .ended:
// This part isn't called if more than 1 player is playing
print("Interruption ENDED")
[player1, player2].forEach { $0?.play() }
#unknown default:
print("Unknown value")
}
}
}
I just ran into the same issue, and it was driving me crazy for a few days. I'm using two AVQueuePlayer (a subclass of AVPlayer) to play two sets of audio sounds on top of each other, and I get the AVAudioSession.interruptionNotification value of .began when there is an incoming call, but there is no .ended notification when the call ends.
That said, I've found that for some reason, .ended is reliably sent if you instead use two instances of AVAudioPlayer. It also works with one instance of AVAudioPlayer mixed with another instance of AVQueuePlayer. But for some reason using two instances of AVQueuePlayer (or AVPlayer) seems to break it.
Did you ever find a solution for this? For my purposes I need queuing of tracks so I must use AVQueuePlayer, so I'll probably file a bug report with Apple.

AVCaptureDeviceInput drops frames after first second running AVCaptureSession, with nativescript

I'm trying to create a video recorder in a nativescript plugin on the ios side which means that I am using the native Objective C Classes inside of the plugin to have a shared interface in the nativescript app with the android implementation.
I have the camera view loaded and I am trying to get access to the video frames from the AVCaptureSession. I created an object that implements the protocol to get the frames and for the first second the function captureOutput which has a parameter DidOutputSampleBuffer outputs the frames. But from then on all the frames are dropped and I do not know why. I can view that they are dropped because the function in the protocol, captureOutput with parameter DidDropSampleBuffer runs for every frame.
I tried changing the order of intializion for the avcapturesession but that didn't change anything.
Below is the main function with the main code to create the capture session and capture object. While this is typescript nativescript allows you to call native Objective C functions and classes so the logic is the same in objective c. I also create a VideoDelegate object in nativescript which corresponds to a class in Objective C which allows me to implement the protocol to get the video frames from the capture device output.
this._captureSession = AVCaptureSession.new();
//Get the camera
this._captureSession.sessionPreset = AVCaptureSessionPreset640x480;
let inputDevice = null;
this._cameraDevice = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo);
//Get the camera input
let error: NSError = null;
this._captureInput = AVCaptureDeviceInput.deviceInputWithDeviceError(this._cameraDevice);
if(this._captureSession.canAddInput(this._captureInput)){
this._captureSession.addInput(this._captureInput);
}
else{
console.log("couldn't add input");
}
let self = this;
const VideoDelegate = (NSObject as any).extend({
captureOutputDidOutputSampleBufferFromConnection(captureOutput: any,sampleBuffer: any, connection:any): void {
console.log("Captureing Frames");
if(self.startRecording){
self._mp4Writer.appendVideoSample(sampleBuffer);
console.log("Appending Video Samples");
}
},
captureOutputDidDropSampleBufferFromConnection(captureOutput: any,sampleBuffer: any, connection:any): void {
console.log("Dropping Frames");
},
videoCameraStarted(date){
// console.log("CAMERA STARTED");
}
}, {
protocols: [AVCaptureVideoDataOutputSampleBufferDelegate]
});
this._videoDelegate = VideoDelegate.new();
//setting up camera output for frames
this._captureOutput = AVCaptureVideoDataOutput.new();
this._captureQueue = dispatch_queue_create("capture Queue", null);
this._captureOutput.setSampleBufferDelegateQueue(this._videoDelegate,this._captureQueue);
this._captureOutput.alwaysDiscardsLateVideoFrames = false;
this._framePixelFormat = NSNumber.numberWithInt(kCVPixelFormatType_32BGRA);
this._captureOutput.videoSettings = NSDictionary.dictionaryWithObjectForKey(this._framePixelFormat,kCVPixelBufferPixelFormatTypeKey);
this._captureSession.addOutput(this._captureOutput);
this._captureSession.startRunning();

SceneKit presentScene(_withTransition:incomingPointOfView completionHandler) crash with dynamically loaded SCNScene

I'm trying to transition from one scene to another, but when I call presentScene there is a crash!
The scenes are not stored in a class or referenced, they are loaded directly into the presentScene call.
Screenshot of crash in Xcode:
My simple minimal project is here: https://dl.dropboxusercontent.com/u/6979623/SceneKitTransitionTest.zip
MKScene is just a subclass of SCNScene, because I would like to know when a scene is deinited to be sure that is it.
self.gameView!.scene = MKScene(named:"art.scnassets/scene1.scn")
then later I call
let scnView:SCNView = self.gameView! as SCNView
let skTransition:SKTransition = SKTransition.crossFadeWithDuration(1.0)
skTransition.pausesIncomingScene = false
skTransition.pausesOutgoingScene = false
self.sceneToggler = !self.sceneToggler
// transition
scnView.presentScene((self.sceneToggler ? MKScene(named:"art.scnassets/scene1.scn")! : MKScene(named:"art.scnassets/scene2.scn")!), withTransition:skTransition, incomingPointOfView:nil, completionHandler:nil)
If I keep a reference to the scene in my class then it works – but that's not what I want. I just want to transition to a different scene and leave the current scene behind deinited.
Why is this crashing?
It seems like a simply task…
That's a bug in SceneKit. Workaround: keep a reference to the outgoing scene before calling "presentScene" and release it after that call.
I was able to reproduce your crash with a somewhat simpler project. It does not use MKScene and does not use notifications to trigger the transition. It crashes on the second attempt to load.
I have filed this at https://bugreport.apple.com as rdar://24012973, which you my wish to dupe, along with your longer project.
Here's my simplified ViewController.swift. Switching between the SCNScene properties (lines 25/29) or on-the-fly loads (lines 24/28) toggles between correct and crashing behavior. That is,
nextScene = SCNScene(named:"art.scnassets/scene2.scn")!
fails, and
nextScene = scene2!
works.
// ViewController.swift
import Cocoa
import SceneKit
import SpriteKit
class ViewController: NSViewController {
#IBOutlet weak var sceneView: SCNView!
private var sceneToggler:Bool = false
private var scene1: SCNScene? = SCNScene(named:"art.scnassets/scene1.scn")
private var scene2: SCNScene? = SCNScene(named:"art.scnassets/scene2.scn")
private func nextSceneToLoad() -> SCNScene {
let nextScene: SCNScene
if (sceneToggler) {
//nextScene = SCNScene(named:"art.scnassets/scene1.scn")!
nextScene = scene1!
print ("scene1")
}
else {
nextScene = SCNScene(named:"art.scnassets/scene2.scn")!
//nextScene = scene2!
print ("scene2")
}
print (nextScene)
sceneToggler = !sceneToggler
return nextScene
}
override func mouseUp(theEvent: NSEvent) {
let skTransition:SKTransition = SKTransition.fadeWithDuration(5.0)
skTransition.pausesIncomingScene = false
skTransition.pausesOutgoingScene = false
sceneView.presentScene(nextSceneToLoad(),
withTransition:skTransition,
incomingPointOfView:nil, completionHandler:nil)
super.mouseUp(theEvent)
}
}

Cocoa - detect event when camera started recording

In my OSX application I'm using code below to show preview from camera.
[[self session] beginConfiguration];
NSError *error = nil;
AVCaptureDeviceInput *newVideoDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:captureDevice error:&error];
if (captureDevice != nil) {
[[self session] removeInput: [self videoDeviceInput]];
if([[self session] canAddInput: newVideoDeviceInput]) {
[[self session] addInput:newVideoDeviceInput];
[self setVideoDeviceInput:newVideoDeviceInput];
} else {
DLog(#"WTF?");
}
}
[[self session] commitConfiguration];
Yet, I need to detect the exact time when the preview from the camera becomes available.
In other words I'm trying to detect the same moment like in Facetime under OSX, where animation starts once the camera provides the preview.
What is the best way to achieve this?
I know this question is really old, but I stumbled upon it too when I was looking for this same question, and I have found answers so here goes.
For starters, AVFoundation is too high level, you'll need to drop down to a lower level, CoreMediaIO. There's not a lot of documentation on this, but basically you need to perform a couple queries.
To do this, we'll use a combination of calls. First, CMIOObjectGetPropertyDataSize lets us get the size of the data we'll query for next, which we can then use when we call CMIOObjectGetPropertyData. To set up the get property data size call, we need to start at the top, using this property address:
var opa = CMIOObjectPropertyAddress(
mSelector: CMIOObjectPropertySelector(kCMIOHardwarePropertyDevices),
mScope: CMIOObjectPropertyScope(kCMIOObjectPropertyScopeGlobal),
mElement: CMIOObjectPropertyElement(kCMIOObjectPropertyElementMaster)
)
Next, we'll set up some variables to keep the data we'll need:
var (dataSize, dataUsed) = (UInt32(0), UInt32(0))
var result = CMIOObjectGetPropertyDataSize(CMIOObjectID(kCMIOObjectSystemObject), &opa, 0, nil, &dataSize)
var devices: UnsafeMutableRawPointer? = nil
From this point on, we'll need to wait until we get some data out, so let's busy loop:
repeat {
if devices != nil {
free(devices)
devices = nil
}
devices = malloc(Int(dataSize))
result = CMIOObjectGetPropertyData(CMIOObjectID(kCMIOObjectSystemObject), &opa, 0, nil, dataSize, &dataUsed, devices);
} while result == OSStatus(kCMIOHardwareBadPropertySizeError)
Once we get past this point in our execution, devices will point to potentially many devices. We need to loop through them, somewhat like this:
if let devices = devices {
for offset in stride(from: 0, to: dataSize, by: MemoryLayout<CMIOObjectID>.size) {
let current = devices.advanced(by: Int(offset)).assumingMemoryBound(to: CMIOObjectID.self)
// current.pointee is your object ID you will want to keep track of somehow
}
}
Finally, clean up devices
free(devices)
Now at this point, you'll want to use that object ID you saved above to make another query. We need a new property address:
var CMIOObjectPropertyAddress(
mSelector: CMIOObjectPropertySelector(kCMIODevicePropertyDeviceIsRunningSomewhere),
mScope: CMIOObjectPropertyScope(kCMIOObjectPropertyScopeWildcard),
mElement: CMIOObjectPropertyElement(kCMIOObjectPropertyElementWildcard)
)
This tells CoreMediaIO that we want to know if the device is currently running somewhere (read: in any app), wildcarding the rest of the fields. Next we get to the meat of the query, camera below corresponds to the ID you saved before:
var (dataSize, dataUsed) = (UInt32(0), UInt32(0))
var result = CMIOObjectGetPropertyDataSize(camera, &opa, 0, nil, &dataSize)
if result == OSStatus(kCMIOHardwareNoError) {
if let data = malloc(Int(dataSize)) {
result = CMIOObjectGetPropertyData(camera, &opa, 0, nil, dataSize, &dataUsed, data)
let on = data.assumingMemoryBound(to: UInt8.self)
// on.pointee != 0 means that it's in use somewhere, 0 means not in use anywhere
}
}
With the above code samples you should have enough to test whether or not the camera is in use. You only need to get the device once (the first part of the answer); the check for if it's in use however, you'll have to do at any time you want this information. As an extra exercise, consider playing with CMIOObjectAddPropertyListenerBlock to be notified on event changes for the in use property address we used above.
While this answer is nearly 3 years too late for the OP, I hope it helps someone in the future. Examples here are given with Swift 3.0.
The previous answer from the user jer is definitely the correct answer, but I just wanted to add one additional important information.
If a listener block is registered with CMIOObjectAddPropertyListenerBlock, the current run loop must be run, otherwise no event will be received and the listener block will never fire.

Notification when display gets connected or disconnected

I'm working on an OS X application that displays custom windows on all available spaces of all the connected displays.
I can get an array of the available display objects by calling [NSScreen screens].
What I'm currently missing is a way of telling if the user connects a display to or disconnects a screen from their system.
I have searched the Cocoa documentation for notifications that deal with a scenario like that without much luck, and I refuse to believe that there isn't some sort of system notification that gets posted when changing the number of displays connected to the system.
Any suggestions on how to solve this problem?
There are several ways to achieve that:
You could implement applicationDidChangeScreenParameters: in your app delegate (the method is part of the NSApplicationDelegateProtocol).
Another way is to listen for the NSApplicationDidChangeScreenParametersNotification sent by the default notification center [NSNotificationCenter defaultCenter].
Whenever your delegate method is called or you receive the notification, you can iterate over [NSScreen screens] and see if a display got connected or removed (you have to maintain a display list you can check against at program launch).
A non-Cocoa approach would be via Core Graphics Display services:
You have to implement a reconfiguration function and register it with CGDisplayRegisterReconfigurationCallback(CGDisplayReconfigurationCallBack cb, void* obj);
In your reconfiguration function you can query the state of the affected display. E.g.:
void DisplayReconfigurationCallBack(CGDirectDisplayID display, CGDisplayChangeSummaryFlags flags, void* userInfo)
{
if(display == someDisplayYouAreInterestedIn)
{
if(flags & kCGDisplaySetModeFlag)
{
...
}
if(flags & kCGDisplayRemoveFlag)
{
...
}
if(flags & kCGDisplayDisabledFlag)
{
...
}
}
if(flags & kCGDisplaySetModeFlag || flags & kCGDisplayDisabledFlag || flags & kCGDisplayRemoveFlag)
{
...
}
}
in swift 3.0:
let nc = NotificationCenter.default
nc.addObserver(self,
selector: #selector(screenDidChange),
name: NSNotification.Name.NSApplicationDidChangeScreenParameters,
object: nil)
NC call back:
final func screenDidChange(notification: NSNotification){
let userInfo = notification.userInfo
print(userInfo)
}