Skip to content

Commit dfb7a97

Browse files
committed
refactor
1 parent 3128147 commit dfb7a97

File tree

2 files changed

+162
-144
lines changed

2 files changed

+162
-144
lines changed

swiftui-previews/swiftui-previews/ContentView.swift

Lines changed: 3 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,21 @@
1414
* limitations under the License.
1515
*/
1616

17-
import CoreText
18-
import Foundation
1917
import LiveKit
2018
import 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-
3220
struct 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-
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright 2026 LiveKit
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import CoreText
18+
import Foundation
19+
import LiveKit
20+
import Observation
21+
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+
32+
@Observable
33+
final class MockVideoSource {
34+
let track: LocalVideoTrack
35+
private var bufferSize: CGSize = .init(width: 640, height: 360)
36+
private var timer: Timer?
37+
38+
func setBufferSize(_ size: CGSize) {
39+
bufferSize = size
40+
}
41+
42+
private var frameCount: Int = 0
43+
private var currentFps: Int = 0
44+
private var framesThisSecond: Int = 0
45+
private var lastFpsUpdate: Date = .init()
46+
47+
init(fps: Double = 30) {
48+
track = LocalVideoTrack.createBufferTrack(name: "preview")
49+
start(fps: fps)
50+
}
51+
52+
deinit {
53+
timer?.invalidate()
54+
}
55+
56+
private func start(fps: Double) {
57+
timer = Timer.scheduledTimer(withTimeInterval: 1.0 / fps, repeats: true) { [weak self] _ in
58+
guard
59+
let self,
60+
let capturer = track.capturer as? BufferCapturer,
61+
bufferSize.width > 0, bufferSize.height > 0
62+
else { return }
63+
64+
// Update FPS counter every second
65+
framesThisSecond += 1
66+
let now = Date()
67+
let elapsed = now.timeIntervalSince(lastFpsUpdate)
68+
if elapsed >= 1.0 {
69+
currentFps = Int(Double(framesThisSecond) / elapsed)
70+
framesThisSecond = 0
71+
lastFpsUpdate = now
72+
}
73+
74+
// Cycle hue over ~5 seconds (at 30fps = 150 frames per cycle)
75+
let hue = CGFloat(frameCount % 150) / 150.0
76+
let color = PlatformColor(hue: hue, saturation: 1.0, brightness: 1.0, alpha: 1.0)
77+
78+
guard let buffer = Self.makePixelBuffer(color: color, frameCount: frameCount, fps: currentFps, size: bufferSize)
79+
else { return }
80+
81+
capturer.capture(buffer)
82+
frameCount += 1
83+
}
84+
}
85+
86+
private static func makePixelBuffer(color: PlatformColor, frameCount: Int, fps: Int, size: CGSize) -> CVPixelBuffer? {
87+
var pixelBuffer: CVPixelBuffer?
88+
let attrs: [String: Any] = [
89+
kCVPixelBufferIOSurfacePropertiesKey as String: [:],
90+
kCVPixelBufferMetalCompatibilityKey as String: true,
91+
kCVPixelBufferCGImageCompatibilityKey as String: true,
92+
kCVPixelBufferCGBitmapContextCompatibilityKey as String: true,
93+
]
94+
CVPixelBufferCreate(
95+
kCFAllocatorDefault,
96+
Int(size.width),
97+
Int(size.height),
98+
kCVPixelFormatType_32BGRA,
99+
attrs as CFDictionary,
100+
&pixelBuffer
101+
)
102+
103+
guard let buffer = pixelBuffer else { return nil }
104+
CVPixelBufferLockBaseAddress(buffer, [])
105+
defer { CVPixelBufferUnlockBaseAddress(buffer, []) }
106+
107+
guard
108+
let base = CVPixelBufferGetBaseAddress(buffer),
109+
let ctx = CGContext(
110+
data: base,
111+
width: Int(size.width),
112+
height: Int(size.height),
113+
bitsPerComponent: 8,
114+
bytesPerRow: CVPixelBufferGetBytesPerRow(buffer),
115+
space: CGColorSpaceCreateDeviceRGB(),
116+
bitmapInfo: CGBitmapInfo.byteOrder32Little.union(
117+
CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)
118+
).rawValue
119+
)
120+
else { return nil }
121+
122+
// Fill background with color
123+
ctx.setFillColor(color.cgColor)
124+
ctx.fill(CGRect(origin: .zero, size: size))
125+
126+
// Draw frame counter, FPS, and resolution (font size ~5% of height)
127+
let fontSize = max(12, min(size.width, size.height) * 0.05)
128+
let font = CTFontCreateUIFontForLanguage(.system, fontSize, nil)!
129+
let attributes: [CFString: Any] = [
130+
kCTFontAttributeName: font,
131+
kCTForegroundColorAttributeName: CGColor(gray: 1.0, alpha: 1.0),
132+
]
133+
134+
let line1Text = "Frame: \(frameCount) | FPS: \(fps)" as CFString
135+
let line1Attr = CFAttributedStringCreate(nil, line1Text, attributes as CFDictionary)!
136+
let line1 = CTLineCreateWithAttributedString(line1Attr)
137+
138+
let line2Text = "\(Int(size.width))×\(Int(size.height))" as CFString
139+
let line2Attr = CFAttributedStringCreate(nil, line2Text, attributes as CFDictionary)!
140+
let line2 = CTLineCreateWithAttributedString(line2Attr)
141+
142+
let line1Bounds = CTLineGetBoundsWithOptions(line1, [])
143+
let line2Bounds = CTLineGetBoundsWithOptions(line2, [])
144+
let lineSpacing: CGFloat = fontSize * 0.3
145+
let totalHeight = line1Bounds.height + line2Bounds.height + lineSpacing
146+
147+
// Center both lines
148+
let y1 = (size.height + totalHeight) / 2 - line1Bounds.height
149+
let y2 = y1 - lineSpacing - line2Bounds.height
150+
151+
ctx.textPosition = CGPoint(x: (size.width - line1Bounds.width) / 2, y: y1)
152+
CTLineDraw(line1, ctx)
153+
154+
ctx.textPosition = CGPoint(x: (size.width - line2Bounds.width) / 2, y: y2)
155+
CTLineDraw(line2, ctx)
156+
157+
return buffer
158+
}
159+
}

0 commit comments

Comments
 (0)