Trying to understand CMTime - ios

I have seen some examples of CMTime (Three separate links), but I still don't get it. I'm using an AVCaptureSession with AVCaptureVideoDataOutput and I want to set the max and min frame rate of the the output. My problem is I just don't understand the CMTime struct.
Apparently CMTimeMake(value, timeScale) should give me value frames every 1/timeScale seconds for a total of value/timeScale seconds, or am I getting that wrong?
Why isn't this documented anywhere in order to explain what this does?
If it does truly work like that, how would I get it to have an indefinite number of frames?
If its really simple, I'm sorry, but nothing has clicked just yet.

A CMTime struct represents a length of time that is stored as rational number (see CMTime Reference). CMTime has a value and a timescale field, and represents the time value/timescale seconds .
CMTimeMake is a function that returns a CMTime structure, for example:
CMTime t1 = CMTimeMake(1, 10); // 1/10 second = 0.1 second
CMTime t2 = CMTimeMake(2, 1); // 2 seconds
CMTime t3 = CMTimeMake(3, 4); // 3/4 second = 0.75 second
CMTime t4 = CMTimeMake(6, 8); // 6/8 second = 0.75 second
The last two time values t3 and t4 represent the same time value, therefore
CMTimeCompare(t3, t4) == 0
If you set the videoMinFrameDuration of a AVCaptureSession is does not make a difference if you set
connection.videoMinFrameDuration = CMTimeMake(1, 20); // or
connection.videoMinFrameDuration = CMTimeMake(2, 40);
In both cases the minimum time interval between frames is set to 1/20 = 0.05 seconds.

My experience differs.
For let testTime = CMTime(seconds: 3.83, preferredTimescale: 100)
If you set a breakpoint and look in the debugger side window it says:
"383 100ths of a second"
Testing by seeking to a fixed offset in a video in AVPlayer has confirmed this.
So put the actual number of seconds in the seconds field, and the precision in the preferredTimescale field. So 100 means precision of hundredths of a second.
Doing
let testTime = CMTime(seconds: 3.83, preferredTimescale: 100)
Still seeks to the same place in the video, but it displays in the debugger side window as "3833 1000ths of a second"
Doing
let testTime = CMTime(seconds: 3.83, preferredTimescale: 1)
Does not seek to the same place in the video, because it's been truncated, and it displays in the debugger side window as "3 seconds". Notice that the .833 part has been lost due to the preferredTimescale.

CMTime(seconds: value, timescale: scale)
means value/scale in a just one second

Related

Trying to append CVPixelBuffers to AVAssetWriterInputPixelBufferAdaptor at the intended framerate

I'm trying to append CVPixelBuffers to AVAssetWriterInputPixelBufferAdaptor at the intended framerate, but it seems to be too fast, and my math is off. This isn't capturing from the camera, but capturing changing images. The actual video is much to fast than the elapsed time it was captured.
I have a function that appends the CVPixelBuffer every 1/24 of a second. So I'm trying to add an offset of 1/24 of a second to the last time.
I've tried:
let sampleTimeOffset = CMTimeMake(value: 100, timescale: 2400)
and:
let sampleTimeOffset = CMTimeMake(value: 24, timescale: 600)
and:
let sampleTimeOffset = CMTimeMakeWithSeconds(0.0416666666, preferredTimescale: 1000000000)
I'm adding onto the currentSampleTime and appending like so:
self.currentSampleTime = CMTimeAdd(currentSampleTime, sampleTimeOffset)
let success = self.assetWriterPixelBufferInput?.append(cv, withPresentationTime: currentSampleTime)
One other solution I thought of is get the difference between the last time and the current time, and add that onto the currentSampleTime for accuracy, but unsure how to do it.
I found a way to accurately capture the time delay by comparing the last time in milliseconds compared to the current time in milliseconds.
First, I have a general current milliseconds time function:
func currentTimeInMilliSeconds()-> Int
{
let currentDate = Date()
let since1970 = currentDate.timeIntervalSince1970
return Int(since1970 * 1000)
}
When I create a writer, (when I start recording video) I set a variable in my class to the current time in milliseconds:
currentCaptureMillisecondsTime = currentTimeInMilliSeconds()
Then in my function that's supposed to be called 1/24 of a second is not always accurate, so I need to get the difference in milliseconds between when I started writing, or my last function call.
Do a conversion of milliseconds to seconds, and set that to CMTimeMakeWithSeconds.
let lastTimeMilliseconds = self.currentCaptureMillisecondsTime
let nowTimeMilliseconds = currentTimeInMilliSeconds()
let millisecondsDifference = nowTimeMilliseconds - lastTimeMilliseconds
// set new current time
self.currentCaptureMillisecondsTime = nowTimeMilliseconds
let millisecondsToSeconds:Float64 = Double(millisecondsDifference) * 0.001
let sampleTimeOffset = CMTimeMakeWithSeconds(millisecondsToSeconds, preferredTimescale: 1000000000)
I can now append my frame with the accurate delay that actually occurred.
self.currentSampleTime = CMTimeAdd(currentSampleTime, sampleTimeOffset)
let success = self.assetWriterPixelBufferInput?.append(cv, withPresentationTime: currentSampleTime)
When I finish writing the video and I save it to my camera roll, it is the exact duration from when I was recording.

Getting Duration of AVPlayer in minutes

I am trying to get the duration of an AVPlayer asset in Hours, Minutes, Seconds. I am able to get the time but it seems to be in seconds and milliseconds.
This is how I get the time:
let duration : CMTime = (player.currentItem!.asset.duration)!
let seconds : Float64 = CMTimeGetSeconds(duration)
I am then applying that to a slider using
slider.maximumValue = Float(seconds)
The outcome of this obviously gives me the duration in seconds however I want to be able to use the duration to set the maximumValue of my slider for video clips which may be under a minute.
For Example: My code above returns 30.865 for a 30 second clip. I need it to return 0.30
This ended up working for me:
let duration : CMTime = (player.currentItem!.asset.duration)!
let timeInMinutes = Float(duration.value)

preferredTimescale in CMTimeMakeWithSeconds

I am implementing custom camera and for that I want to set exposure duration.
My code for setting slider's properties is-
slider.maximumValue = Float(CMTimeGetSeconds(camera.activeFormat.maxExposureDuration))
slider.minimumValue = Float(CMTimeGetSeconds(camera.activeFormat.minExposureDuration))
Now the problem comes while setting the exposure time whenever the slider is changed.
My code for that looks like this -
change(duration: CMTimeMakeWithSeconds(Double(slider.value), 600), iso: AVCaptureISOCurrent)
But in
func CMTimeMakeWithSeconds(_ seconds: Float64, _ preferredTimescale: Int32) -> CMTime
I am confused with preferredTimescale, and what should be its value, it's working fine with 600, but what is ideal value.
You should view the discussion here
Preferred time scale will add a denominator to your seconds, meaning if you set 5 seconds and preferredTimeScale of 60 it will be 1/12 of a second

How do I get current playing time, CMTime in milliseconds in AVPlayer?

I am getting current playing value in seconds but i need in milliseconds. I tried to currentTime.value/currentTime.scale. But it didn't get exact value.
CMTime currentTime = vPlayer.currentItem.currentTime; //playing time
CMTimeValue tValue=currentTime.value;
CMTimeScale tScale=currentTime.timescale;
NSTimeInterval time = CMTimeGetSeconds(currentTime);
NSLog(#"Time :%f",time);//This is in seconds, it misses decimal value double shot=(float)tValue/(float)tScale;
shotTimeVideo=[NSString stringWithFormat:#"%.2f",(float)tValue/(float)tScale];
CMTime currentTime = vPlayer.currentItem.currentTime; //playing time
CMTimeValue tValue=currentTime.value;
CMTimeScale tScale=currentTime.timescale;
NSTimeInterval time = CMTimeGetSeconds(currentTime);
NSLog(#"Time :%f",time);//This is in seconds, it misses decimal value
double shot=(float)tValue/(float)tScale;
shotTimeVideo=[NSString stringWithFormat:#"%.2f", (float)tValue/(float)tScale];
okay,first of all, the value you want is millisecond not seconds
So,you can just use CMTimeGetSeconds(<#CMTime time#>) to get Secondsthen,if you want millisecond , use seconds / 1000.f for float or double value
for CMTime calculating use CMTime method CMTimeMultiplyByRatio(<#CMTime time#>, <#int32_t multiplier#>, <#int32_t divisor#>)just do this --> CMTimeMultiplyByRatio(yourCMTimeValue, 1, 1000)
Apple's Doc
#function CMTimeMultiplyByRatio
#abstract Returns the result of multiplying a CMTime by an integer, then dividing by another integer.
#discussion The exact rational value will be preserved, if possible without overflow. If an overflow
would occur, a new timescale will be chosen so as to minimize the rounding error.
Default rounding will be applied when converting the result to this timescale. If the
result value still overflows when timescale == 1, then the result will be either positive
or negative infinity, depending on the direction of the overflow.
If any rounding occurs for any reason, the result's kCMTimeFlags_HasBeenRounded flag will be
set. This flag will also be set if the CMTime operand has kCMTimeFlags_HasBeenRounded set.
If the denominator, and either the time or the numerator, are zero, the result will be
kCMTimeInvalid. If only the denominator is zero, the result will be either kCMTimePositiveInfinity
or kCMTimeNegativeInfinity, depending on the signs of the other arguments.
If time is invalid, the result will be invalid. If time is infinite, the result will be
similarly infinite. If time is indefinite, the result will be indefinite.
#result (time * multiplier) / divisor
A little late, but may be useful for others.
You can get the timestamp in milliseconds from the CMTime object by
CMTimeConvertScale(yourCMTime, timescale:1000, method:
CMTimeRoundingMethod.roundHalfAwayFromZero
An example:
var yourtime:CMTime = CMTimeMake(value:1234567, timescale: 10)
var timemilli:CMTime = CMTimeConvertScale(yourtime, timescale:1000, method:
CMTimeRoundingMethod.roundHalfAwayFromZero)
var timemillival:Int64 = timemilli.value
print("timemilli \(timemillival)")
which will produce
timemilli 123456700

AVAsset video adjust duration

Given a list of CMSampleBuffers that have been read in from an asset, I want to adjust the duration of the asset so that it's half length (twice the speed) of the original.
Currently my function for generating new time stamps looks like:
func adjustTimeStampsForBuffers(buffers: [CMSampleBuffer]) -> [CMTime] {
let frameCount = buffers.count
// self.duration is CMTimeGetSeconds(asset.duration)
let increment = Float(self.duration / 2) / Float(frameCount)
return Array(0.stride(to: frameCount, by: 1)).enumerate().map {
let seconds: Float64 = Float64(increment) * Float64($0.index)
return CMTimeMakeWithSeconds(seconds, self.asset.duration.timescale)
}
}
however this doesn't seem to work and the outputted assets are in fact twice the length, not half. Can anybody point out where I'm going wrong?
Edit:
Thanks to #sschale, here's my final answer:
func adjustTimeStampsForBuffers(buffers: [CMSampleBuffer]) -> [CMTime] {
return buffers.map {
let time = CMSampleBufferGetPresentationTimeStamp($0)
return CMTimeMake(time.value, time.timescale * 2)
}
}
Instead of calculating new values, the timestamp is adjusted instead.
Based on my reading of the docs, it looks like that self.asset.duration.timescale may be the key here, as changing it will influence the whole file (if I'm understanding the reference you're making that that timescale is for the whole file, or maybe you need to adjust it in each of the buffers).
See here for more info as well.
Relevant section:
A CMTime is represented as a rational number, with a numerator (an
int64_t value), and a denominator (an int32_t timescale).
Conceptually, the timescale specifies the fraction of a second each
unit in the numerator occupies. Thus if the timescale is 4, each unit
represents a quarter of a second; if the timescale is 10, each unit
represents a tenth of a second, and so on. In addition to a simple
time value, a CMTime can represent non-numeric values: +infinity,
-infinity, and indefinite. Using a flag CMTime indicates whether the time been rounded at some point.
CMTimes contain an epoch number, which is usually set to 0, but can be
used to distinguish unrelated timelines: for example, it could be
incremented each time through a presentation loop, to differentiate
between time N in loop 0 from time N in loop 1

Resources