FAQ - MKS2508/MKS-IPTV-App GitHub Wiki
Technical FAQ for developers and advanced users of MKS-IPTV-App.
A: Swift 6's Actor model provides several advantages for IPTV streaming:
- Data race safety: Actors prevent concurrent access to mutable state
- Structured concurrency: async/await provides cleaner error handling
- Performance: Actor isolation reduces lock contention
- Debugging: Easier to reason about concurrent operations
// Traditional approach (prone to data races)
class StreamManager {
private var streams: [UUID: AVPlayer] = [:]
private let queue = DispatchQueue(label: "streams")
func addStream(_ id: UUID, player: AVPlayer) {
queue.async {
self.streams[id] = player // Potential race condition
}
}
}
// Actor-based approach (data race safe)
actor StreamManager {
private var streams: [UUID: AVPlayer] = [:]
func addStream(_ id: UUID, player: AVPlayer) {
streams[id] = player // Compiler-enforced safety
}
}
A: The HTTP proxy solves several IPTV-specific challenges:
- Header manipulation: Many IPTV services require specific User-Agent or Referrer headers
- CORS issues: Bypasses cross-origin restrictions for web-based players
- URL obfuscation: Handles complex authentication and token-based URLs
- Protocol bridging: Converts HTTP streams to formats compatible with AVPlayer
// Example: Stream requires VLC User-Agent
let originalURL = "http://example.com/stream.m3u8"
let proxiedURL = "http://127.0.0.1:8080/proxy"
// Proxy adds required headers automatically
request.setValue("VLC/3.0.0", forHTTPHeaderField: "User-Agent")
request.setValue("http://example.com", forHTTPHeaderField: "Referrer")
A: MOV conversion provides Apple ecosystem optimizations:
- AirPlay compatibility: Better streaming to Apple TV
- Metadata support: Enhanced metadata for Apple's media frameworks
- Seek performance: Optimized for Apple's video players
- Codec optimization: Uses Apple's hardware-accelerated codecs
A: The StreamManager implements a multi-layered recovery strategy:
actor StreamRecoveryManager {
enum RecoveryStrategy {
case seekToLive // For live streams
case reloadPlayerItem // For temporary network issues
case completeRestart // For persistent failures
}
func attemptRecovery(for sessionId: UUID) async {
// Try strategies in order of increasing severity
for strategy in [.seekToLive, .reloadPlayerItem, .completeRestart] {
if await tryRecovery(strategy, for: sessionId) {
break
}
}
}
}
Recovery mechanisms:
- Seek to live edge: For live streams that fall behind
- Player item reload: For temporary network hiccups
- Complete restart: For persistent failures
- URL re-resolution: For expired or changed stream URLs
A: The system uses several heuristics to determine proxy necessity:
func requiresProxy(_ url: URL) -> Bool {
// Check for HTTP (vs HTTPS) - often needs headers
if url.scheme == "http" {
return true
}
// Check for known IPTV providers that require headers
let iptvProviders = ["example.tv", "stream.provider"]
if iptvProviders.contains(where: { url.host?.contains($0) == true }) {
return true
}
// Check for query parameters indicating auth tokens
if url.query?.contains("token") == true {
return true
}
// Check for non-standard ports
if let port = url.port, ![80, 443, 8080].contains(port) {
return true
}
return false
}
A: The app adapts to different formats through format detection and appropriate handling:
enum StreamFormat {
case hls // .m3u8 files
case dash // .mpd files
case directMP4 // Direct video files
case unknown
static func detect(from url: URL) -> StreamFormat {
let pathExtension = url.pathExtension.lowercased()
switch pathExtension {
case "m3u8": return .hls
case "mpd": return .dash
case "mp4", "mov", "avi", "mkv": return .directMP4
default: return .unknown
}
}
var requiresProxy: Bool {
switch self {
case .hls, .dash: return true // Often need custom headers
case .directMP4: return false // Usually work directly
case .unknown: return true // Safe default
}
}
}
A: The download system uses URLSession's built-in resume capabilities with persistent state:
actor DownloadManager {
func pauseDownload(_ downloadId: UUID) async {
guard let task = activeDownloads[downloadId] else { return }
// Cancel and get resume data
task.urlSessionTask?.cancel { resumeData in
Task {
await self.saveResumeData(downloadId, data: resumeData)
}
}
// Update status
task.status = .paused
await saveDownloadState()
}
func resumeDownload(_ downloadId: UUID) async throws {
guard let task = activeDownloads[downloadId],
let resumeData = await getResumeData(downloadId) else {
throw DownloadError.cannotResume
}
// Create new task with resume data
let newTask = downloadSession.downloadTask(withResumeData: resumeData)
task.urlSessionTask = newTask
task.status = .downloading
newTask.resume()
}
}
A: The local HTTP server provides several benefits for downloaded content:
- Streaming large files: Avoid loading entire files into memory
- Seek support: Enable video scrubbing and chapter navigation
- Range requests: Efficient partial content delivery
- Format agnostic: Works with any video format
- AVPlayer compatibility: Standard HTTP interface that AVPlayer expects
// Stream downloaded file through local server
let downloadedFile = "/path/to/downloaded/video.mp4"
let streamingURL = "http://127.0.0.1:8080/\(downloadId)"
// AVPlayer can now stream the local file with seek support
let player = AVPlayer(url: streamingURL)
A: Multiple integrity checks are performed:
struct DownloadIntegrityChecker {
func verifyDownload(_ downloadId: UUID) async throws {
guard let task = await getDownloadTask(downloadId) else {
throw DownloadError.taskNotFound
}
// 1. File size verification
let expectedSize = task.totalBytes
let actualSize = try getFileSize(task.localPath)
guard expectedSize == actualSize else {
throw DownloadError.sizeMismatch
}
// 2. File format validation
let asset = AVAsset(url: task.localPath)
let isReadable = try await asset.load(.isReadable)
guard isReadable else {
throw DownloadError.corruptFile
}
// 3. Checksum verification (if available)
if let expectedHash = task.expectedChecksum {
let actualHash = try await calculateChecksum(task.localPath)
guard expectedHash == actualHash else {
throw DownloadError.checksumMismatch
}
}
}
}
A: The app uses SwiftUI's adaptive layout system with custom view modifiers:
struct AdaptiveContentView: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.verticalSizeClass) var verticalSizeClass
var body: some View {
Group {
if horizontalSizeClass == .compact {
// iPhone portrait: Stack layout
VStack {
VideoPlayerView()
ControlsView()
}
} else {
// iPad/iPhone landscape: Side-by-side
HStack {
VideoPlayerView()
ControlsView()
}
}
}
.adaptToDeviceOrientation()
}
}
extension View {
func adaptToDeviceOrientation() -> some View {
self.modifier(OrientationAdaptiveModifier())
}
}
A: TouchBar integration uses AppKit's NSTouchBar APIs bridged to SwiftUI:
class TouchBarManager: NSObject, NSTouchBarDelegate {
@MainActor
static func configureTouchBar(for window: NSWindow) {
let touchBar = NSTouchBar()
touchBar.delegate = TouchBarManager()
touchBar.defaultItemIdentifiers = [
.playPause,
.volume,
.search,
.flexibleSpace,
.categories
]
window.touchBar = touchBar
}
func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
switch identifier {
case .playPause:
return createPlayPauseItem()
case .volume:
return createVolumeSlider()
case .search:
return createSearchField()
default:
return nil
}
}
}
A: tvOS focus management uses focus guides and preferredFocusEnvironments:
struct tvOSContentView: View {
@FocusState private var focusedItem: FocusableItem?
var body: some View {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(items) { item in
ItemView(item: item)
.focused($focusedItem, equals: .item(item.id))
.focusable()
}
}
}
.focusGuide(.automatic)
.onPlayPauseCommand {
handlePlayPause()
}
.onMenuPressedCommand {
handleMenuPress()
}
}
enum FocusableItem: Hashable {
case item(UUID)
case searchField
case settings
}
}
A: Testing actors requires careful consideration of isolation:
@MainActor
class StreamManagerTests: XCTestCase {
var streamManager: StreamManager!
override func setUp() async throws {
streamManager = StreamManager()
}
func testStreamLoading() async throws {
// Test actor methods with async/await
let testURL = URL(string: "https://test.stream")!
let sessionId = try await streamManager.loadStream(testURL)
// Verify results
let session = await streamManager.getSession(sessionId)
XCTAssertNotNil(session)
}
func testConcurrentAccess() async throws {
// Test thread safety with concurrent operations
let urls = (0..<10).map { URL(string: "https://test\($0).stream")! }
// Start multiple streams concurrently
let sessionIds = try await withThrowingTaskGroup(of: UUID.self) { group in
for url in urls {
group.addTask {
try await self.streamManager.loadStream(url)
}
}
var results: [UUID] = []
for try await sessionId in group {
results.append(sessionId)
}
return results
}
XCTAssertEqual(sessionIds.count, urls.count)
}
}
A: The app uses protocol-based dependency injection for testability:
protocol NetworkService {
func fetchData(from url: URL) async throws -> Data
}
class ProductionNetworkService: NetworkService {
func fetchData(from url: URL) async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
}
class MockNetworkService: NetworkService {
var mockData: Data?
var shouldThrowError = false
func fetchData(from url: URL) async throws -> Data {
if shouldThrowError {
throw URLError(.networkConnectionLost)
}
return mockData ?? Data()
}
}
// In tests
class StreamManagerTests: XCTestCase {
func testWithMockNetwork() async throws {
let mockNetwork = MockNetworkService()
mockNetwork.mockData = "test stream data".data(using: .utf8)
let streamManager = StreamManager(networkService: mockNetwork)
// Test with predictable mock data
}
}
A: Use a combination of Instruments and custom memory tracking:
#if DEBUG
class MemoryTracker: ObservableObject {
@Published var allocatedActors: [String: Int] = [:]
@Published var allocatedViews: [String: Int] = [:]
private let queue = DispatchQueue(label: "memory.tracker")
func trackActor<T>(_ actor: T) {
let typeName = String(describing: type(of: actor))
queue.async {
self.allocatedActors[typeName, default: 0] += 1
DispatchQueue.main.async {
self.objectWillChange.send()
}
}
}
func trackView<T: View>(_ view: T) {
let typeName = String(describing: type(of: view))
queue.async {
self.allocatedViews[typeName, default: 0] += 1
DispatchQueue.main.async {
self.objectWillChange.send()
}
}
}
var totalMemoryUsage: String {
let info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size)/4
let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
}
}
if kerr == KERN_SUCCESS {
let memory = Float(info.resident_size) / 1024.0 / 1024.0
return String(format: "%.1f MB", memory)
}
return "Unknown"
}
}
// Usage in SwiftUI views
struct DebugMemoryView: View {
@StateObject private var memoryTracker = MemoryTracker()
var body: some View {
VStack {
Text("Memory: \(memoryTracker.totalMemoryUsage)")
List {
Section("Actors") {
ForEach(memoryTracker.allocatedActors.sorted(by: <), id: \.key) { key, value in
Text("\(key): \(value)")
}
}
Section("Views") {
ForEach(memoryTracker.allocatedViews.sorted(by: <), id: \.key) { key, value in
Text("\(key): \(value)")
}
}
}
}
}
}
#endif
A: The app dynamically adapts based on device capabilities:
actor DeviceCapabilityManager {
struct DeviceCapabilities {
let maxConcurrentDownloads: Int
let maxVideoResolution: CGSize
let supportsHardwareDecoding: Bool
let maxBufferSize: TimeInterval
}
static func detectCapabilities() -> DeviceCapabilities {
let device = UIDevice.current
let processInfo = ProcessInfo.processInfo
// Determine based on device model and available memory
let totalMemory = processInfo.physicalMemory
let deviceModel = device.model
switch (deviceModel, totalMemory) {
case (_, let memory) where memory >= 8_000_000_000: // 8GB+
return DeviceCapabilities(
maxConcurrentDownloads: 4,
maxVideoResolution: CGSize(width: 3840, height: 2160), // 4K
supportsHardwareDecoding: true,
maxBufferSize: 60.0
)
case (_, let memory) where memory >= 4_000_000_000: // 4GB+
return DeviceCapabilities(
maxConcurrentDownloads: 3,
maxVideoResolution: CGSize(width: 1920, height: 1080), // 1080p
supportsHardwareDecoding: true,
maxBufferSize: 30.0
)
default: // Lower-end devices
return DeviceCapabilities(
maxConcurrentDownloads: 2,
maxVideoResolution: CGSize(width: 1280, height: 720), // 720p
supportsHardwareDecoding: false,
maxBufferSize: 15.0
)
}
}
}
A: The app implements proactive memory management:
actor MemoryPressureManager {
private var memoryPressureSource: DispatchSourceMemoryPressure?
func startMonitoring() {
memoryPressureSource = DispatchSource.makeMemoryPressureSource(
eventMask: [.warning, .critical],
queue: .global()
)
memoryPressureSource?.setEventHandler { [weak self] in
Task {
await self?.handleMemoryPressure()
}
}
memoryPressureSource?.resume()
}
private func handleMemoryPressure() async {
// Stop non-essential downloads
await DownloadManager.shared.pauseNonEssentialDownloads()
// Reduce video buffer size
await StreamManager.shared.reduceBufferSizes()
// Clear caches
URLCache.shared.removeAllCachedResponses()
// Notify UI to update if needed
await MainActor.run {
NotificationCenter.default.post(
name: .memoryPressureDetected,
object: nil
)
}
}
}
A: The app uses secure storage and runtime protection:
actor SecureConfiguration {
private let keychain = Keychain(service: "com.mks.iptv.secure")
func storeAPIKey(_ key: String) async throws {
try keychain.set(key, key: "api_key")
}
func getAPIKey() async throws -> String? {
return try keychain.get("api_key")
}
// Runtime obfuscation for hardcoded values
var userAgent: String {
let encoded = "TUtTLUlQVFYtQXBwLzEuMA==" // Base64 encoded
return String(data: Data(base64Encoded: encoded)!, encoding: .utf8)!
}
}
A: Downloaded files are protected through multiple layers:
actor ContentProtectionManager {
func protectDownloadedFile(at path: URL) async throws {
// 1. Set file attributes to prevent external access
var resourceValues = URLResourceValues()
resourceValues.isExcludedFromBackup = true
try path.setResourceValues(resourceValues)
// 2. Encrypt sensitive content (optional)
if shouldEncryptContent {
try await encryptFile(at: path)
}
// 3. Set file permissions (macOS)
#if os(macOS)
try FileManager.default.setAttributes(
[.posixPermissions: 0o600], // Owner read/write only
ofItemAtPath: path.path
)
#endif
}
private func encryptFile(at path: URL) async throws {
// Implementation depends on encryption requirements
// Could use CryptoKit for local encryption
}
}
Remember: This FAQ covers technical implementation details. For user-facing questions, refer to the main documentation at MKS2508.github.io/MKS-IPTV-App.
{{< include _Footer.md >}}