もしCADisplayLinkの周期が不安定だったら
1. CADisplayLinkとは
デバイスのリフレッシュレートに連動するタイマーオブジェクトです。
詳細はApple Developer Documentationを参照してください。
A timer object that allows your app to synchronize its drawing to the refresh rate of the display.
CADisplayLinkは、単にAVFoundationを用いてビデオ再生するのではなく、ビデオからフレームを抽出してReality Kitなどで細かい周期でそのフレームを投影する場合などに利用されます。
例えば、60fpsで再生するには約16.66msecごとにフレーム(画像)を連続して投影する必要があり、その周期制御にCADisplayLinkを用います。
2. 使用例
細かい文法や実装例については、公式ドキュメントやGitHubに掲載されているサンプルコードを参照してください。
以下は簡単な設定例です。
class VideoPlaybackService {
private var displayLink: CADisplayLink?
func setCADisplayLink() {
displayLink = CADisplayLink(target: self, selector: #selector(displayNextFrame(link:)))
let frameRate = 60.0
// このように指定することで厳密に周期を指定できます
displayLink?.preferredFrameRateRange = .init(
minimum: Float(frameRate),
maximum: Float(frameRate),
preferred: Float(frameRate)
)
displayLink?.add(to: .main, forMode: .common)
}
@objc private func displayNextFrame(link: CADisplayLink) {
// フレーム処理など
}
}
使い始めた当初は displayLink?.preferredFrameRateRange = 60.0
のように指定していましたが、周期がずれる問題があったため、記述方法を上記のように変更しました。
3. CADisplayLinkの周期が守られない?
上記のコード例のように設定しても、CADisplayLinkの周期が期待通りに動作しない場合があります。
公式ドキュメントを確認しながら、原因を推測してみましょう。
詳細はこちらを参照してください。
デバイス自体のリフレッシュレートがその周期をサポートしていない
a display link could invoke your callback 60 times per second for a display with a refresh rate of 60 hertz. However, the display link could invoke your callback less frequently, such as 30, 20, or 15 hertz, by setting a range with smaller values.
デバイスのリフレッシュレートが60Hzの場合、呼び出し可能な周期は30, 20, 15Hzといった、元のリフレッシュレートを整数で割った値となります。
つまり、リフレッシュレートが90Hzのデバイスでは、CADisplayLinkで使用できる周期は90, 45, 30Hzとなり、60Hzは設定できません。
なお、Apple Vision Proでは100, 96, 90Hzがサポートされており、120や60Hzは利用できないため、その周期で呼び出すことはできません。
Mainスレッドがヘビータスクでブロックされている
The display link makes a best attempt to invoke your app’s callback within the frequency range you set to this property. However, the system also takes into account the device’s hardware capabilities and the other tasks your game or app is running.
Choose a frame rate range that your app can consistently maintain.
上記のコード例では、displayLink?.add(to: .main, forMode: .common)
と記述しており、これはCADisplayLinkがMainスレッド上で動作することを意味します。
もちろん、これをBackgroundスレッドで実行する方法もありますが、そもそもCADisplayLinkの周期を阻害するほどの重い処理は、できる限りMainスレッドで実行しないようにするべきです。
自分の場合は、VTDecompressionSessionを使ったデコード処理をMainスレッドで実行していたため、CADisplayLinkの周期が不安定になっていました。
そのため、CADisplayLinkで呼び出される重い処理は、別スレッド(バックグラウンド)で実行するようにしましょう。
class VideoPlaybackService {
private var displayLink: CADisplayLink?
private let decodingQueue = DispatchQueue(label: "com.yourapp.decodingQueue", qos: .userInitiated)
func setCADisplayLink() {
displayLink = CADisplayLink(target: self, selector: #selector(displayNextFrame(link:)))
let frameRate = 60.0
// このように指定することで厳密に周期を指定できます
displayLink?.preferredFrameRateRange = .init(
minimum: Float(frameRate),
maximum: Float(frameRate),
preferred: Float(frameRate)
)
displayLink?.add(to: .main, forMode: .common)
}
@objc private func displayNextFrame(link: CADisplayLink) {
decodingQueue.async { [weak self] in
// フレーム処理など
}
}
}
これで狙い通りの周期でCADisplayLinkが実行されるようになりました。
Discussion