1414 * limitations under the License.
1515 */
1616
17- import CoreText
18- import Foundation
1917import LiveKit
2018import SwiftUI
2119
22- #if canImport(UIKit)
23- import UIKit
24-
25- typealias PlatformColor = UIColor
26- #elseif canImport(AppKit)
27- import AppKit
28-
29- typealias PlatformColor = NSColor
30- #endif
31-
3220struct ContentView : View {
33- @State private var mock = MockVideoSource ( )
21+ @State private var mockVideoSource = MockVideoSource ( )
3422 @Environment ( \. displayScale) private var displayScale
3523
3624 var body : some View {
3725 VStack ( spacing: 20 ) {
3826 Text ( " LiveKitSDK: v \( LiveKitSDK . version) " )
3927 GeometryReader { geometry in
40- SwiftUIVideoView ( mock . track, renderMode: . sampleBuffer)
28+ SwiftUIVideoView ( mockVideoSource . track, renderMode: . sampleBuffer)
4129 . clipShape ( RoundedRectangle ( cornerRadius: 12 ) )
4230 . onChange ( of: geometry. size, initial: true ) { _, newSize in
43- mock . setBufferSize ( CGSize (
31+ mockVideoSource . setBufferSize ( CGSize (
4432 width: newSize. width * displayScale,
4533 height: newSize. height * displayScale
4634 ) )
@@ -54,132 +42,3 @@ struct ContentView: View {
5442#Preview {
5543 ContentView ( )
5644}
57-
58- @Observable
59- final class MockVideoSource {
60- let track : LocalVideoTrack
61- private var bufferSize : CGSize = . init( width: 640 , height: 360 )
62- private var timer : Timer ?
63-
64- func setBufferSize( _ size: CGSize ) {
65- bufferSize = size
66- }
67-
68- private var frameCount : Int = 0
69- private var currentFps : Int = 0
70- private var framesThisSecond : Int = 0
71- private var lastFpsUpdate : Date = . init( )
72-
73- init ( fps: Double = 30 ) {
74- track = LocalVideoTrack . createBufferTrack ( name: " preview " )
75- start ( fps: fps)
76- }
77-
78- deinit {
79- timer? . invalidate ( )
80- }
81-
82- private func start( fps: Double ) {
83- timer = Timer . scheduledTimer ( withTimeInterval: 1.0 / fps, repeats: true ) { [ weak self] _ in
84- guard
85- let self,
86- let capturer = track. capturer as? BufferCapturer ,
87- bufferSize. width > 0 , bufferSize. height > 0
88- else { return }
89-
90- // Update FPS counter every second
91- framesThisSecond += 1
92- let now = Date ( )
93- let elapsed = now. timeIntervalSince ( lastFpsUpdate)
94- if elapsed >= 1.0 {
95- currentFps = Int ( Double ( framesThisSecond) / elapsed)
96- framesThisSecond = 0
97- lastFpsUpdate = now
98- }
99-
100- // Cycle hue over ~5 seconds (at 30fps = 150 frames per cycle)
101- let hue = CGFloat ( frameCount % 150 ) / 150.0
102- let color = PlatformColor ( hue: hue, saturation: 1.0 , brightness: 1.0 , alpha: 1.0 )
103-
104- guard let buffer = Self . makePixelBuffer ( color: color, frameCount: frameCount, fps: currentFps, size: bufferSize)
105- else { return }
106-
107- capturer. capture ( buffer)
108- frameCount += 1
109- }
110- }
111-
112- private static func makePixelBuffer( color: PlatformColor , frameCount: Int , fps: Int , size: CGSize ) -> CVPixelBuffer ? {
113- var pixelBuffer : CVPixelBuffer ?
114- let attrs : [ String : Any ] = [
115- kCVPixelBufferIOSurfacePropertiesKey as String : [ : ] ,
116- kCVPixelBufferMetalCompatibilityKey as String : true ,
117- kCVPixelBufferCGImageCompatibilityKey as String : true ,
118- kCVPixelBufferCGBitmapContextCompatibilityKey as String : true ,
119- ]
120- CVPixelBufferCreate (
121- kCFAllocatorDefault,
122- Int ( size. width) ,
123- Int ( size. height) ,
124- kCVPixelFormatType_32BGRA,
125- attrs as CFDictionary ,
126- & pixelBuffer
127- )
128-
129- guard let buffer = pixelBuffer else { return nil }
130- CVPixelBufferLockBaseAddress ( buffer, [ ] )
131- defer { CVPixelBufferUnlockBaseAddress ( buffer, [ ] ) }
132-
133- guard
134- let base = CVPixelBufferGetBaseAddress ( buffer) ,
135- let ctx = CGContext (
136- data: base,
137- width: Int ( size. width) ,
138- height: Int ( size. height) ,
139- bitsPerComponent: 8 ,
140- bytesPerRow: CVPixelBufferGetBytesPerRow ( buffer) ,
141- space: CGColorSpaceCreateDeviceRGB ( ) ,
142- bitmapInfo: CGBitmapInfo . byteOrder32Little. union (
143- CGBitmapInfo ( rawValue: CGImageAlphaInfo . premultipliedFirst. rawValue)
144- ) . rawValue
145- )
146- else { return nil }
147-
148- // Fill background with color
149- ctx. setFillColor ( color. cgColor)
150- ctx. fill ( CGRect ( origin: . zero, size: size) )
151-
152- // Draw frame counter, FPS, and resolution (font size ~5% of height)
153- let fontSize = max ( 12 , min ( size. width, size. height) * 0.05 )
154- let font = CTFontCreateUIFontForLanguage ( . system, fontSize, nil ) !
155- let attributes : [ CFString : Any ] = [
156- kCTFontAttributeName: font,
157- kCTForegroundColorAttributeName: CGColor ( gray: 1.0 , alpha: 1.0 ) ,
158- ]
159-
160- let line1Text = " Frame: \( frameCount) | FPS: \( fps) " as CFString
161- let line1Attr = CFAttributedStringCreate ( nil , line1Text, attributes as CFDictionary ) !
162- let line1 = CTLineCreateWithAttributedString ( line1Attr)
163-
164- let line2Text = " \( Int ( size. width) ) × \( Int ( size. height) ) " as CFString
165- let line2Attr = CFAttributedStringCreate ( nil , line2Text, attributes as CFDictionary ) !
166- let line2 = CTLineCreateWithAttributedString ( line2Attr)
167-
168- let line1Bounds = CTLineGetBoundsWithOptions ( line1, [ ] )
169- let line2Bounds = CTLineGetBoundsWithOptions ( line2, [ ] )
170- let lineSpacing : CGFloat = fontSize * 0.3
171- let totalHeight = line1Bounds. height + line2Bounds. height + lineSpacing
172-
173- // Center both lines
174- let y1 = ( size. height + totalHeight) / 2 - line1Bounds. height
175- let y2 = y1 - lineSpacing - line2Bounds. height
176-
177- ctx. textPosition = CGPoint ( x: ( size. width - line1Bounds. width) / 2 , y: y1)
178- CTLineDraw ( line1, ctx)
179-
180- ctx. textPosition = CGPoint ( x: ( size. width - line2Bounds. width) / 2 , y: y2)
181- CTLineDraw ( line2, ctx)
182-
183- return buffer
184- }
185- }
0 commit comments