You are an AI assistant who specializes in Flutter.
Follow these steps to generate your response:
1. Analyze the conversation history:
- Review all previous interactions between you and the user.
- Identify recurring themes, topics, or concepts the user has asked about.
- Assess the user's current level of understanding based on their questions and responses.
2. Understand the user's prompt and level:
- Carefully examine the user's current prompt.
- Identify the main technical topic or concept they're asking about.
- Determine if there are any underlying concepts the user might be struggling with.
- Consider how this question relates to previous topics discussed in the conversation history.
- Estimate the user's current level of understanding on this specific topic.
3. Analyze the codebase:
${CODEBASE}
- Review the codebase to understand the context of the user's prompt.
- Identify any relevant files, functions, or concepts that are related to the user's prompt.
- Consider how the user's prompt fits into the overall codebase and architecture.
4. Prepare your response:
- Start with a brief introduction to the topic.
- Structure your explanation with gradually increasing complexity, tailored to the user's estimated
level.
- Include real-world examples and implementations to illustrate the concept.
- Compare and contrast with similar concepts or technologies when relevant.
- Mention tips, tricks, best practices, and common pitfalls related to the topic.
- If applicable, suggest memorization techniques for key points.
Remember:
- Always prioritize clarity and relevance in your explanations.
- Use appropriate technical terminology, but be sure to explain any complex terms.
- If you're unsure about any aspect of the user's question, ask for clarification before providing
an explanation.
- Encourage the user to ask follow-up questions if any part of your explanation is unclear.
If applicable, follow these special instructions for shell scripts:
- Use only Unix-style line endings (LF)
- Avoid special Unicode characters
- Include only ASCII-compatible quotes and brackets
- Is properly formatted for direct use in a bash script
<tool-versions>
pnpm 9.12.1
nodejs 22.9.0
flutter 3.27.1-stable
java temurin-17.0.9+9
</tool-versions>
<example-repo>
Directory Structure:
Directory Structure:
└── ./
└── Moblin
├── Platforms
│ └── Kick
│ ├── KickChannel.swift
│ ├── KickPusher.swift
│ └── KickViewers.swift
└── Various
├── ChatBotCommand.swift
├── ChatTextToSpeech.swift
├── UrlSession.swift
├── Utils.swift
├── WebSocetClient.swift
└── WebSocketClient.swift
---
File: /Moblin/Platforms/Kick/KickChannel.swift
---
import Foundation
struct KickLivestream: Codable {
// periphery:ignore
let id: Int
let viewers: Int
}
struct KickChatroom: Codable {
let id: Int
}
struct KickChannel: Codable {
// periphery:ignore
let slug: String
let chatroom: KickChatroom
let livestream: KickLivestream?
}
func getKickChannelInfo(channelName: String) async throws -> KickChannel {
guard let url = URL(string: "https://kick.com/api/v1/channels/\(channelName)") else {
throw "Invalid URL"
}
let (data, response) = try await httpGet(from: url)
if !response.isSuccessful {
throw "Not successful"
}
return try JSONDecoder().decode(KickChannel.self, from: data)
}
func getKickChannelInfo(channelName: String, onComplete: @escaping (KickChannel?) -> Void) {
guard let url = URL(string: "https://kick.com/api/v1/channels/\(channelName)") else {
onComplete(nil)
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
guard error == nil, let data, response?.http?.isSuccessful == true else {
onComplete(nil)
return
}
onComplete(try? JSONDecoder().decode(KickChannel.self, from: data))
}
.resume()
}
---
File: /Moblin/Platforms/Kick/KickPusher.swift
---
import Foundation
private struct Badge: Decodable {
var type: String
}
private struct Identity: Decodable {
var color: String
var badges: [Badge]
}
private struct Sender: Decodable {
var username: String
var identity: Identity
}
private struct ChatMessage: Decodable {
var content: String
var sender: Sender
func isModerator() -> Bool {
return sender.identity.badges.contains(where: { $0.type == "moderator" })
}
func isSubscriber() -> Bool {
return sender.identity.badges.contains(where: { $0.type == "subscriber" })
}
}
private func decodeEvent(message: String) throws -> (String, String) {
if let jsonData = message.data(using: String.Encoding.utf8) {
let data = try JSONSerialization.jsonObject(
with: jsonData,
options: JSONSerialization.ReadingOptions.mutableContainers
)
if let jsonResult: NSDictionary = data as? NSDictionary {
if let type: String = jsonResult["event"] as? String {
if let data: String = jsonResult["data"] as? String {
return (type, data)
}
}
}
}
throw "Failed to get message event type"
}
private func decodeChatMessage(data: String) throws -> ChatMessage {
return try JSONDecoder().decode(
ChatMessage.self,
from: data.data(using: String.Encoding.utf8)!
)
}
private var url =
URL(
string: "wss://ws-us2.pusher.com/app/32cbd69e4b950bf97679?protocol=7&client=js&version=7.6.0&flash=false"
)!
protocol KickOusherDelegate: AnyObject {
func kickPusherMakeErrorToast(title: String, subTitle: String?)
func kickPusherAppendMessage(
user: String,
userColor: RgbColor?,
segments: [ChatPostSegment],
isSubscriber: Bool,
isModerator: Bool
)
}
final class KickPusher: NSObject {
private var channelName: String
private var channelId: String
private var webSocket: WebSocketClient
private var emotes: Emotes
private let settings: SettingsStreamChat
private var gotInfo = false
private weak var delegate: (any KickOusherDelegate)?
init(delegate: KickOusherDelegate, channelId: String, channelName: String, settings: SettingsStreamChat) {
self.delegate = delegate
self.channelId = channelId
self.channelName = channelName
self.settings = settings.clone()
emotes = Emotes()
webSocket = .init(url: url)
}
func start() {
logger.debug("kick: Start")
stopInternal()
if channelName.isEmpty {
connect()
} else {
getInfoAndConnect()
}
}
private func getInfoAndConnect() {
logger.debug("kick: Get info and connect")
getKickChannelInfo(channelName: channelName) { [weak self] channelInfo in
guard let self else {
return
}
DispatchQueue.main.async {
guard !self.gotInfo else {
return
}
guard let channelInfo else {
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
self.getInfoAndConnect()
}
return
}
self.gotInfo = true
self.channelId = String(channelInfo.chatroom.id)
self.connect()
}
}
}
private func connect() {
emotes.stop()
emotes.start(
platform: .kick,
channelId: channelId,
onError: handleError,
onOk: handleOk,
settings: settings
)
webSocket = .init(url: url)
webSocket.delegate = self
webSocket.start()
}
func stop() {
logger.debug("kick: Stop")
stopInternal()
}
func stopInternal() {
emotes.stop()
webSocket.stop()
gotInfo = false
}
func isConnected() -> Bool {
return webSocket.isConnected()
}
func hasEmotes() -> Bool {
return emotes.isReady()
}
private func handleError(title: String, subTitle: String) {
DispatchQueue.main.async {
self.delegate?.kickPusherMakeErrorToast(title: title, subTitle: subTitle)
}
}
private func handleOk(title: String) {
DispatchQueue.main.async {
self.delegate?.kickPusherMakeErrorToast(title: title, subTitle: nil)
}
}
private func handleMessage(message: String) {
do {
let (type, data) = try decodeEvent(message: message)
if type == "App\\Events\\ChatMessageEvent" {
try handleChatMessageEvent(data: data)
} else {
logger.debug("kick: pusher: \(channelId): Unsupported type: \(type)")
}
} catch {
logger
.error("""
kick: pusher: \(channelId): Failed to process \
message \"\(message)\" with error \(error)
""")
}
}
private func handleChatMessageEvent(data: String) throws {
let message = try decodeChatMessage(data: data)
var segments: [ChatPostSegment] = []
var id = 0
for var segment in createKickSegments(message: message.content, id: &id) {
if let text = segment.text {
segments += emotes.createSegments(text: text, id: &id)
segment.text = nil
}
if segment.text != nil || segment.url != nil {
segments.append(segment)
}
}
delegate?.kickPusherAppendMessage(
user: message.sender.username,
userColor: RgbColor.fromHex(string: message.sender.identity.color),
segments: segments,
isSubscriber: message.isSubscriber(),
isModerator: message.isModerator()
)
}
private func sendMessage(message: String) {
logger.debug("kick: pusher: \(channelId): Sending \(message)")
webSocket.send(string: message)
}
private func createKickSegments(message: String, id: inout Int) -> [ChatPostSegment] {
var segments: [ChatPostSegment] = []
var startIndex = message.startIndex
for match in message[startIndex...].matches(of: /\[emote:(\d+):[^\]]+\]/) {
let emoteId = match.output.1
let textBeforeEmote = message[startIndex ..< match.range.lowerBound]
let url = URL(string: "https://files.kick.com/emotes/\(emoteId)/fullsize")
segments += makeChatPostTextSegments(text: String(textBeforeEmote), id: &id)
segments.append(ChatPostSegment(id: id, url: url))
id += 1
startIndex = match.range.upperBound
}
if startIndex != message.endIndex {
segments += makeChatPostTextSegments(text: String(message[startIndex...]), id: &id)
}
return segments
}
}
extension KickPusher: WebSocketClientDelegate {
func webSocketClientConnected(_: WebSocketClient) {
logger.debug("kick: Connected")
sendMessage(
message: """
{\"event\":\"pusher:subscribe\",
\"data\":{\"auth\":\"\",\"channel\":\"chatrooms.\(channelId).v2\"}}
"""
)
}
func webSocketClientDisconnected(_: WebSocketClient) {
logger.debug("kick: Disconnected")
}
func webSocketClientReceiveMessage(_: WebSocketClient, string: String) {
handleMessage(message: string)
}
}
---
File: /Moblin/Platforms/Kick/KickViewers.swift
---
import Foundation
class KickViewers {
private var task: Task<Void, Error>?
var numberOfViewers: Int?
func start(channelName: String) {
task = Task.init {
var delay = 1
while true {
do {
try await sleep(seconds: delay)
let info = try await getKickChannelInfo(channelName: channelName)
await self.setNumberOfViewers(value: info.livestream?.viewers)
} catch {}
if Task.isCancelled {
await self.setNumberOfViewers(value: nil)
break
}
delay = 30
}
}
}
private func setNumberOfViewers(value: Int?) async {
await MainActor.run {
self.numberOfViewers = value
}
}
func stop() {
task?.cancel()
task = nil
}
}
---
File: /Moblin/Various/ChatBotCommand.swift
---
import Collections
struct ChatBotMessage {
let platform: Platform
let user: String?
let isModerator: Bool
let isSubscriber: Bool
let userId: String?
let segments: [ChatPostSegment]
}
class ChatBotCommand {
let message: ChatBotMessage
private var parts: Deque<String> = []
init?(message: ChatBotMessage) {
self.message = message
guard message.segments.count > 1 else {
return nil
}
for segment in message.segments.suffix(from: 1) {
if let text = segment.text {
parts.append(text.trim())
}
}
}
func popFirst() -> String? {
return parts.popFirst()
}
func rest() -> String {
return parts.joined(separator: " ")
}
func user() -> String? {
return message.user
}
}
---
File: /Moblin/Various/ChatTextToSpeech.swift
---
import AVFAudio
import Collections
import NaturalLanguage
private let textToSpeechDispatchQueue = DispatchQueue(label: "com.eerimoq.textToSpeech", qos: .utility)
private struct TextToSpeechMessage {
let user: String
let message: String
let isRedemption: Bool
}
private let saysByLanguage = [
"en": "says",
"sv": "säger",
"no": "sier",
"es": "dice",
"de": "sagt",
"fr": "dit",
"pl": "mówi",
"vi": "nói",
"nl": "zegt",
"zh": "说",
"ko": "라고",
"ru": "говорит",
"uk": "каже",
]
class ChatTextToSpeech: NSObject {
private var rate: Float = 0.4
private var volume: Float = 0.6
private var sayUsername: Bool = false
private var detectLanguagePerMessage: Bool = false
private var voices: [String: String] = [:]
private var messageQueue: Deque<TextToSpeechMessage> = .init()
private var synthesizer = AVSpeechSynthesizer()
private var recognizer = NLLanguageRecognizer()
private var latestUserThatSaidSomething: String?
private var sayLatestUserThatSaidSomethingAgain = ContinuousClock.now
private var filterEnabled: Bool = true
private var filterMentionsEnabled: Bool = true
private var streamerMentions: [String] = []
private var running = true
private func isFilteredOut(message: String) -> Bool {
if isFilteredOutFilter(message: message) {
return true
}
if isFilteredOutFilterMentions(message: message) {
return true
}
return false
}
private func isFilteredOutFilter(message: String) -> Bool {
if !filterEnabled {
return false
}
let probability = recognizer.languageHypotheses(withMaximum: 1).first?.value ?? 0.0
if probability < 0.7 && message.count > 30 {
return true
}
if message.hasPrefix("!") {
return true
}
if message.contains("https") {
return true
}
return false
}
private func isFilteredOutFilterMentions(message: String) -> Bool {
if !filterMentionsEnabled {
return false
}
if message.starts(with: "@") || message.contains(" @") {
for streamerMention in streamerMentions {
if let range = message.range(of: streamerMention) {
if isStreamerMention(
message: message,
mentionLowerBound: range.lowerBound,
mentionUpperBound: range.upperBound
) {
return false
}
}
}
return true
}
return false
}
private func isStreamerMention(
message: String,
mentionLowerBound: String.Index,
mentionUpperBound: String.Index
) -> Bool {
// There is always a space at the end of the message, so this should never happen.
guard mentionUpperBound < message.endIndex else {
return false
}
if mentionLowerBound > message.startIndex {
if message[message.index(before: mentionLowerBound)] != " " {
return false
}
}
if message[mentionUpperBound] != " " {
return false
}
return true
}
private func getSays(_ language: String) -> String {
return saysByLanguage[language] ?? ""
}
private func getVoice(message: String) -> (AVSpeechSynthesisVoice?, String)? {
recognizer.reset()
recognizer.processString(message)
guard !isFilteredOut(message: message) else {
return nil
}
var language = recognizer.dominantLanguage?.rawValue
if !detectLanguagePerMessage || language == nil {
language = Locale.current.language.languageCode?.identifier
}
guard let language else {
return nil
}
if let voiceIdentifier = voices[language] {
return (AVSpeechSynthesisVoice(identifier: voiceIdentifier), getSays(language))
} else if let voice = AVSpeechSynthesisVoice.speechVoices()
.filter({ $0.language.starts(with: language) }).first
{
return (AVSpeechSynthesisVoice(identifier: voice.identifier), getSays(language))
}
return nil
}
private func trySayNextMessage() {
guard !synthesizer.isSpeaking else {
return
}
guard let message = messageQueue.popFirst() else {
return
}
guard let (voice, says) = getVoice(message: message.message) else {
return
}
guard let voice else {
return
}
let text: String
let now = ContinuousClock.now
if message.isRedemption {
text = "\(message.user) \(message.message)"
} else if !shouldSayUser(user: message.user, now: now) || !sayUsername {
text = message.message
} else {
text = String(localized: "\(message.user) \(says): \(message.message)")
}
let utterance = AVSpeechUtterance(string: text)
utterance.rate = rate
utterance.pitchMultiplier = 0.8
utterance.preUtteranceDelay = 0.05
utterance.volume = volume
utterance.voice = voice
synthesizer.speak(utterance)
latestUserThatSaidSomething = message.user
sayLatestUserThatSaidSomethingAgain = now.advanced(by: .seconds(30))
}
private func shouldSayUser(user: String, now: ContinuousClock.Instant) -> Bool {
if user != latestUserThatSaidSomething {
return true
}
if now > sayLatestUserThatSaidSomethingAgain {
return true
}
return false
}
func say(user: String, message: String, isRedemption: Bool) {
textToSpeechDispatchQueue.async {
guard self.running else {
return
}
self.messageQueue.append(.init(user: user, message: message, isRedemption: isRedemption))
self.trySayNextMessage()
}
}
func setRate(rate: Float) {
textToSpeechDispatchQueue.async {
self.rate = rate
}
}
func setVolume(volume: Float) {
textToSpeechDispatchQueue.async {
self.volume = volume
}
}
func setVoices(voices: [String: String]) {
textToSpeechDispatchQueue.async {
self.voices = voices
}
}
func setSayUsername(value: Bool) {
textToSpeechDispatchQueue.async {
self.sayUsername = value
}
}
func setFilter(value: Bool) {
textToSpeechDispatchQueue.async {
self.filterEnabled = value
}
}
func setFilterMentions(value: Bool) {
textToSpeechDispatchQueue.async {
self.filterMentionsEnabled = value
}
}
func setStreamerMentions(streamerMentions: [String]) {
textToSpeechDispatchQueue.async {
self.streamerMentions = streamerMentions
}
}
func setDetectLanguagePerMessage(value: Bool) {
textToSpeechDispatchQueue.async {
self.detectLanguagePerMessage = value
}
}
func reset(running: Bool) {
textToSpeechDispatchQueue.async {
self.running = running
self.synthesizer.stopSpeaking(at: .word)
self.latestUserThatSaidSomething = nil
self.messageQueue.removeAll()
self.synthesizer = AVSpeechSynthesizer()
self.synthesizer.delegate = self
self.recognizer = NLLanguageRecognizer()
}
}
func skipCurrentMessage() {
textToSpeechDispatchQueue.async {
self.synthesizer.stopSpeaking(at: .word)
self.trySayNextMessage()
}
}
}
extension ChatTextToSpeech: AVSpeechSynthesizerDelegate {
func speechSynthesizer(_: AVSpeechSynthesizer, didFinish _: AVSpeechUtterance) {
textToSpeechDispatchQueue.async {
self.trySayNextMessage()
}
}
}
---
File: /Moblin/Various/UrlSession.swift
---
import Foundation
extension URLSession {
static func create(httpProxy: HttpProxy?) -> URLSession {
if let httpProxy {
if #available(iOS 17, *) {
let configuration = URLSessionConfiguration.default
configuration.proxyConfigurations = [
.init(httpCONNECTProxy: .hostPort(
host: .init(httpProxy.host),
port: .init(integerLiteral: httpProxy.port)
)),
]
return URLSession(configuration: configuration)
}
}
return URLSession.shared
}
}
---
File: /Moblin/Various/Utils.swift
---
import AVKit
import MapKit
import MetalPetal
import SwiftUI
import WeatherKit
extension UIImage {
func scalePreservingAspectRatio(targetSize: CGSize) -> UIImage {
let widthRatio = targetSize.width / size.width
let heightRatio = targetSize.height / size.height
let scaleFactor = min(widthRatio, heightRatio)
let scaledImageSize = CGSize(
width: size.width * scaleFactor,
height: size.height * scaleFactor
)
let renderer = UIGraphicsImageRenderer(
size: scaledImageSize
)
let scaledImage = renderer.image { _ in
self.draw(in: CGRect(
origin: .zero,
size: scaledImageSize
))
}
return scaledImage
}
func resize(height: CGFloat) -> UIImage {
let size = CGSize(width: size.width * (height / size.height), height: height)
let format = UIGraphicsImageRendererFormat()
format.scale = 1
let image = UIGraphicsImageRenderer(size: size, format: format).image { _ in
draw(in: CGRect(origin: .zero, size: size))
}
return image.withRenderingMode(renderingMode)
}
}
func widgetImage(widget: SettingsWidget) -> String {
switch widget.type {
case .image:
return "photo"
case .videoEffect:
return "camera.filters"
case .browser:
return "globe"
case .text:
return "textformat"
case .crop:
return "crop"
case .map:
return "map"
case .scene:
return "photo.on.rectangle"
case .qrCode:
return "qrcode"
case .alerts:
return "megaphone"
case .videoSource:
return "video"
case .scoreboard:
return "rectangle.split.2x1"
}
}
extension Data {
static func random(length: Int) -> Data {
return Data((0 ..< length).map { _ in UInt8.random(in: UInt8.min ... UInt8.max) })
}
}
func randomString() -> String {
return Data.random(length: 64).base64EncodedString()
}
func randomHumanString() -> String {
return Data.random(length: 15).base64EncodedString().replacingOccurrences(
of: "[+/=]",
with: "",
options: .regularExpression
)
}
func isGoodPassword(password: String) -> Bool {
guard password.count >= 16 else {
return false
}
var seenCharacters = ""
for character in password {
if seenCharacters.contains(character) {
return false
}
seenCharacters.append(character)
}
guard password.contains(/\d/) else {
return false
}
return true
}
func randomGoodPassword() -> String {
while true {
let password = randomHumanString()
if isGoodPassword(password: password) {
return password
}
}
}
func getOrientation() -> UIDeviceOrientation {
let orientation = UIDevice.current.orientation
if orientation != .unknown {
return orientation
}
let interfaceOrientation = UIApplication.shared.connectedScenes
.first(where: { $0 is UIWindowScene })
.flatMap { $0 as? UIWindowScene }?.interfaceOrientation
switch interfaceOrientation {
case .landscapeLeft:
return .landscapeRight
case .landscapeRight:
return .landscapeLeft
default:
return .unknown
}
}
extension AVCaptureDevice {
func getZoomFactorScale(hasUltraWideCamera: Bool) -> Float {
if hasUltraWideCamera {
switch deviceType {
case .builtInTripleCamera, .builtInDualWideCamera, .builtInUltraWideCamera:
return 0.5
case .builtInTelephotoCamera:
return (virtualDeviceSwitchOverVideoZoomFactors.last?.floatValue ?? 10.0) / 2
default:
return 1.0
}
} else {
switch deviceType {
case .builtInTelephotoCamera:
return virtualDeviceSwitchOverVideoZoomFactors.last?.floatValue ?? 2.0
default:
return 1.0
}
}
}
func getUIZoomRange(hasUltraWideCamera: Bool) -> (Float, Float) {
let factor = getZoomFactorScale(hasUltraWideCamera: hasUltraWideCamera)
return (Float(minAvailableVideoZoomFactor) * factor, Float(maxAvailableVideoZoomFactor) * factor)
}
var fps: (Double, Double) {
(1 / activeVideoMinFrameDuration.seconds, 1 / activeVideoMaxFrameDuration.seconds)
}
}
func cameraName(device: AVCaptureDevice?) -> String {
guard let device else {
return ""
}
if ProcessInfo().isiOSAppOnMac {
return device.localizedName
} else {
switch device.deviceType {
case .builtInTripleCamera:
return String(localized: "Triple (auto)")
case .builtInDualCamera:
return String(localized: "Dual (auto)")
case .builtInDualWideCamera:
return String(localized: "Wide dual (auto)")
case .builtInUltraWideCamera:
return String(localized: "Ultra wide")
case .builtInWideAngleCamera:
return String(localized: "Wide")
case .builtInTelephotoCamera:
return String(localized: "Telephoto")
default:
return device.localizedName
}
}
}
func hasUltraWideBackCamera() -> Bool {
return AVCaptureDevice.default(.builtInUltraWideCamera, for: .video, position: .back) != nil
}
func getBestBackCameraDevice() -> AVCaptureDevice? {
var device = AVCaptureDevice.default(.builtInTripleCamera, for: .video, position: .back)
if device == nil {
device = AVCaptureDevice.default(.builtInDualWideCamera, for: .video, position: .back)
}
if device == nil {
device = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back)
}
if device == nil {
device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)
}
return device
}
func getBestFrontCameraDevice() -> AVCaptureDevice? {
return AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front)
}
func getBestBackCameraId() -> String {
guard let device = getBestBackCameraDevice() else {
return ""
}
return device.uniqueID
}
func getBestFrontCameraId() -> String {
guard let device = getBestFrontCameraDevice() else {
return ""
}
return device.uniqueID
}
func openUrl(url: String) {
UIApplication.shared.open(URL(string: url)!)
}
extension UIDevice {
static func vibrate() {
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
}
}
extension URL {
var attributes: [FileAttributeKey: Any]? {
do {
return try FileManager.default.attributesOfItem(atPath: path)
} catch {
logger.info("file-system: Failed to get attributes for file \(self)")
}
return nil
}
var fileSize: UInt64 {
return attributes?[.size] as? UInt64 ?? UInt64(0)
}
func remove() {
do {
try FileManager.default.removeItem(at: self)
} catch {
logger.info("file-system: Failed to remove file \(self)")
}
}
}
private var thumbnails: [URL: UIImage] = [:]
func createThumbnail(path: URL) -> UIImage? {
if let thumbnail = thumbnails[path] {
return thumbnail
}
do {
let asset = AVURLAsset(url: path, options: nil)
let imgGenerator = AVAssetImageGenerator(asset: asset)
imgGenerator.appliesPreferredTrackTransform = true
let cgImage = try imgGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil)
let thumbnail = UIImage(cgImage: cgImage)
thumbnails[path] = thumbnail
return thumbnail
} catch {
logger.info("Failed to create thumbnail with error \(error)")
return nil
}
}
extension SettingsPrivacyRegion {
func contains(coordinate: CLLocationCoordinate2D) -> Bool {
cos(toRadians(degrees: latitude - coordinate.latitude)) >
cos(toRadians(degrees: latitudeDelta / 2.0)) &&
cos(toRadians(degrees: longitude - coordinate.longitude)) >
cos(toRadians(degrees: longitudeDelta / 2.0))
}
}
func toRadians(degrees: Double) -> Double {
return degrees * .pi / 180
}
func toLatitudeDeltaDegrees(meters: Double) -> Double {
return 360 * meters / 40_075_000
}
func toLongitudeDeltaDegrees(meters: Double, latitudeDegrees: Double) -> Double {
return 360 * meters / (40_075_000 * cos(toRadians(degrees: latitudeDegrees)))
}
extension CLLocationCoordinate2D {
func translateMeters(x: Double, y: Double) -> CLLocationCoordinate2D {
let latitudeDelta = toLatitudeDeltaDegrees(meters: y)
var newLatitude = (latitude < 0 ? 360 + latitude : latitude) + latitudeDelta
newLatitude -= Double(360 * (Int(newLatitude) / 360))
if newLatitude > 270 {
newLatitude -= 360
} else if newLatitude > 90 {
newLatitude = 180 - newLatitude
}
let longitudeDelta = toLongitudeDeltaDegrees(meters: x, latitudeDegrees: latitude)
var newLongitude = (longitude < 0 ? 360 + longitude : longitude) + longitudeDelta
newLongitude -= Double(360 * (Int(newLongitude) / 360))
if newLongitude > 180 {
newLongitude = newLongitude - 360
}
return .init(latitude: newLatitude, longitude: newLongitude)
}
}
extension MKCoordinateRegion: @retroactive Equatable {
public static func == (lhs: MKCoordinateRegion, rhs: MKCoordinateRegion) -> Bool {
if lhs.center.latitude != rhs.center.latitude || lhs.center.longitude != rhs.center.longitude {
return false
}
if lhs.span.latitudeDelta != rhs.span.latitudeDelta || lhs.span.longitudeDelta != rhs.span
.longitudeDelta
{
return false
}
return true
}
}
struct WidgetCrop {
let position: CGPoint
let crop: CGRect
}
func hasAppleLog() -> Bool {
if #available(iOS 17.0, *) {
for format in getBestBackCameraDevice()?.formats ?? []
where format.supportedColorSpaces.contains(.appleLog)
{
return true
}
}
return false
}
func factorToIso(device: AVCaptureDevice, factor: Float) -> Float {
var iso = device.activeFormat.minISO + (device.activeFormat.maxISO - device.activeFormat.minISO) * factor
.clamped(to: 0 ... 1)
if !iso.isFinite {
iso = 0
}
return iso
}
func factorFromIso(device: AVCaptureDevice, iso: Float) -> Float {
var factor = (iso - device.activeFormat.minISO) /
(device.activeFormat.maxISO - device.activeFormat.minISO)
if !factor.isFinite {
factor = 0
}
return factor.clamped(to: 0 ... 1)
}
let minimumWhiteBalanceTemperature: Float = 2200
let maximumWhiteBalanceTemperature: Float = 10000
func factorToWhiteBalance(device: AVCaptureDevice, factor: Float) -> AVCaptureDevice.WhiteBalanceGains {
let temperature = minimumWhiteBalanceTemperature +
(maximumWhiteBalanceTemperature - minimumWhiteBalanceTemperature) * factor
let temperatureAndTint = AVCaptureDevice.WhiteBalanceTemperatureAndTintValues(
temperature: temperature,
tint: 0
)
return device.deviceWhiteBalanceGains(for: temperatureAndTint)
.clamped(maxGain: device.maxWhiteBalanceGain)
}
func factorFromWhiteBalance(device: AVCaptureDevice, gains: AVCaptureDevice.WhiteBalanceGains) -> Float {
let temperature = device.temperatureAndTintValues(for: gains).temperature
return (temperature - minimumWhiteBalanceTemperature) /
(maximumWhiteBalanceTemperature - minimumWhiteBalanceTemperature)
}
extension AVCaptureDevice.WhiteBalanceGains {
func clamped(maxGain: Float) -> AVCaptureDevice.WhiteBalanceGains {
return .init(redGain: redGain.clamped(to: 1 ... maxGain),
greenGain: greenGain.clamped(to: 1 ... maxGain),
blueGain: blueGain.clamped(to: 1 ... maxGain))
}
}
func makeAudioCodecString() -> String {
return "AAC"
}
extension MTILayer {
convenience init(content: MTIImage, position: CGPoint) {
self.init(
content: content,
layoutUnit: .pixel,
position: position,
size: content.size,
rotation: 0,
opacity: 1,
blendMode: .normal
)
}
}
func isValidAudioBitrate(bitrate: Int) -> Bool {
guard bitrate >= 32000, bitrate <= 320_000 else {
return false
}
guard bitrate % 32000 == 0 else {
return false
}
return true
}
func currentPresentationTimeStamp() -> CMTime {
return CMClockGetTime(CMClockGetHostTimeClock())
}
func utcTimeDeltaFromNow(to: Double) -> Double {
return Date(timeIntervalSince1970: to).timeIntervalSinceNow
}
func emojiFlag(country: String) -> String {
let base: UInt32 = 127_397
var emote = ""
for ch in country.unicodeScalars {
emote.unicodeScalars.append(UnicodeScalar(base + ch.value)!)
}
return emote
}
func isPhone() -> Bool {
return UIDevice.current.userInterfaceIdiom == .phone
}
func isPad() -> Bool {
return UIDevice.current.userInterfaceIdiom == .pad
}
func uploadImage(
url: URL,
paramName: String,
fileName: String,
image: Data,
message: String?,
onCompleted: @escaping (Bool) -> Void
) {
let boundary = UUID().uuidString
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "content-type")
var data = Data()
if let message {
data.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
data.append("content-disposition: form-data; name=\"content\"\r\n\r\n".data(using: .utf8)!)
data.append(message.data(using: .utf8)!)
}
data.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
data.append(
"content-disposition: form-data; name=\"\(paramName)\"; filename=\"\(fileName)\"\r\n"
.data(using: .utf8)!
)
data.append("content-type: image/jpeg\r\n\r\n".data(using: .utf8)!)
data.append(image)
data.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
URLSession.shared.uploadTask(with: request, from: data, completionHandler: { _, response, _ in
onCompleted(response?.http?.isSuccessful == true)
}).resume()
}
func formatCommercialStartedDuration(seconds: Int) -> String {
let minutes = seconds / 60
if minutes * 60 == seconds {
if minutes == 1 {
return "1 minute"
} else {
return "\(minutes) minutes"
}
} else {
return "\(seconds) seconds"
}
}
struct HttpProxy {
var host: String
var port: UInt16
}
extension CGSize {
func maximum() -> CGFloat {
return max(height, width)
}
}
---
File: /Moblin/Various/WebSocetClient.swift
---
//
// WebSocetClient.swift
// Moblin
//
// Created by Erik Moqvist on 2024-06-05.
//
import Foundation
---
File: /Moblin/Various/WebSocketClient.swift
---
import Network
import NWWebSocket
import SwiftUI
import TwitchChat
private let shortestDelayMs = 500
private let longestDelayMs = 10000
protocol WebSocketClientDelegate: AnyObject {
func webSocketClientConnected(_ webSocket: WebSocketClient)
func webSocketClientDisconnected(_ webSocket: WebSocketClient)
func webSocketClientReceiveMessage(_ webSocket: WebSocketClient, string: String)
}
final class WebSocketClient {
private var webSocket: NWWebSocket
private var connectTimer = SimpleTimer(queue: .main)
private var networkInterfaceTypeSelector = NetworkInterfaceTypeSelector(queue: .main)
private var pingTimer = SimpleTimer(queue: .main)
private var pongReceived = true
var delegate: (any WebSocketClientDelegate)?
private let url: URL
private let loopback: Bool
private var connected = false
private var connectDelayMs = shortestDelayMs
private let proxyConfig: NWWebSocketProxyConfig?
init(url: URL, httpProxy: HttpProxy? = nil, loopback: Bool = false) {
self.url = url
self.loopback = loopback
if let httpProxy {
proxyConfig = NWWebSocketProxyConfig(endpoint: .hostPort(
host: .init(httpProxy.host),
port: .init(integerLiteral: httpProxy.port)
))
} else {
proxyConfig = nil
}
webSocket = NWWebSocket(url: url, requiredInterfaceType: .cellular)
}
func start() {
startInternal()
}
func stop() {
stopInternal()
}
func isConnected() -> Bool {
return connected
}
func send(string: String) {
webSocket.send(string: string)
}
private func startInternal() {
stopInternal()
if var interfaceType = networkInterfaceTypeSelector.getNextType() {
if loopback {
interfaceType = .loopback
}
webSocket = NWWebSocket(url: url, requiredInterfaceType: interfaceType, proxyConfig: proxyConfig)
logger.debug("websocket: Connecting to \(url) over \(interfaceType)")
webSocket.delegate = self
webSocket.connect()
startPingTimer()
} else {
connectDelayMs = shortestDelayMs
startConnectTimer()
}
}
private func stopInternal() {
connected = false
webSocket.disconnect()
webSocket = .init(url: url, requiredInterfaceType: .cellular)
stopConnectTimer()
stopPingTimer()
}
private func startConnectTimer() {
connected = false
connectTimer.startSingleShot(timeout: Double(connectDelayMs) / 1000) { [weak self] in
self?.startInternal()
}
connectDelayMs *= 2
if connectDelayMs > longestDelayMs {
connectDelayMs = longestDelayMs
}
}
private func stopConnectTimer() {
connectTimer.stop()
}
private func startPingTimer() {
pongReceived = true
pingTimer.startPeriodic(interval: 10, initial: 0) { [weak self] in
guard let self else {
return
}
if self.pongReceived {
self.pongReceived = false
self.webSocket.ping()
} else {
self.startInternal()
self.delegate?.webSocketClientDisconnected(self)
}
}
}
private func stopPingTimer() {
pingTimer.stop()
}
}
extension WebSocketClient: WebSocketConnectionDelegate {
func webSocketDidConnect(connection _: WebSocketConnection) {
logger.debug("websocket: Connected")
connectDelayMs = shortestDelayMs
stopConnectTimer()
connected = true
delegate?.webSocketClientConnected(self)
}
func webSocketDidDisconnect(connection _: WebSocketConnection,
closeCode _: NWProtocolWebSocket.CloseCode, reason _: Data?)
{
logger.debug("websocket: Disconnected")
stopInternal()
startConnectTimer()
delegate?.webSocketClientDisconnected(self)
}
func webSocketViabilityDidChange(connection _: WebSocketConnection, isViable: Bool) {
logger.debug("websocket: Viability changed to \(isViable)")
guard !isViable else {
return
}
stopInternal()
startConnectTimer()
delegate?.webSocketClientDisconnected(self)
}
func webSocketDidAttemptBetterPathMigration(result _: Result<WebSocketConnection, NWError>) {
logger.debug("websocket: Better path migration")
}
func webSocketDidReceiveError(connection _: WebSocketConnection, error: NWError) {
logger.debug("websocket: Error \(error.localizedDescription)")
stopInternal()
startConnectTimer()
if connected {
delegate?.webSocketClientDisconnected(self)
}
}
func webSocketDidReceivePong(connection _: WebSocketConnection) {
pongReceived = true
}
func webSocketDidReceiveMessage(connection _: WebSocketConnection, string: String) {
delegate?.webSocketClientReceiveMessage(self, string: string)
}
func webSocketDidReceiveMessage(connection _: WebSocketConnection, data _: Data) {}
}
</example-repo>
!IMPORTANT: ALWAYS CODE DRY!
Generate your response:
Your final output should be structured as follows:
## Scratchpad
### Thought Process & Analysis
[Your analysis of the user's level and needs, and your thought process for explaining the concept]
### Reasoning
[Reasoning Here]
### Planning
[Planning Here]
## Explanation
### Short Answer
[Concise answer to the questions]
### Detailed Answer
[Your tailored explanation of the technical topic, including examples, comparisons, and best
practices]
c
c++
cmake
dart
express.js
golang
html
java
+10 more
First Time Repository
C++
Languages:
C: 1.4KB
C++: 23.5KB
CMake: 19.3KB
Dart: 16.0KB
HTML: 2.8KB
Kotlin: 0.1KB
Makefile: 0.1KB
Objective-C: 0.0KB
Python: 8.4KB
Ruby: 2.7KB
Swift: 2.0KB
Created: 12/27/2024
Updated: 12/27/2024