// // Immersive180VideoViewModel.swift // TestVideoLeakOneImmersiveView // import SwiftUI import AVFoundation import AVKit import OSLog import RealityKitContent import RealityKit class Immersive180VideoViewModel { private let logger = Logger(subsystem: SUBSYSTEM, category: "Immersive180VideoView") private var url: URL private let avPlayer = AVPlayer() init(url: URL) { logger.info("\(#function) \(#line)") self.url = url } func setupContentEntity() -> Entity { logger.info("\(#function) \(#line)") let playerItem = AVPlayerItem(url: url) avPlayer.replaceCurrentItem(with: playerItem) let material = VideoMaterial(avPlayer: avPlayer) let hemisphereMesh = generateHemisphere(radius: 20.0) let sphere = ModelEntity(mesh: hemisphereMesh) sphere.model?.materials = [material] let blackMaterial = SimpleMaterial(color: .black, roughness: .float(1), isMetallic: false) let outerSphereMesh = MeshResource.generateSphere(radius: 15) let outerSphere = ModelEntity(mesh: outerSphereMesh) outerSphere.model?.materials = [blackMaterial] outerSphere.position = [0, 0, 0] outerSphere.scale = .init(x: 1E3, y: 1E3, z: 1E3) let contentEntity = Entity() contentEntity.scale *= .init(x: -1, y: 1, z: 1) contentEntity.addChild(outerSphere) contentEntity.addChild(sphere) // Display the video in front of the user by rotating the entity 180 degrees around the Y-axis let rotation = simd_quatf(angle: .pi, axis: SIMD3(x: 0, y: 1, z: 0)) contentEntity.transform.rotation = rotation NotificationCenter.default.addObserver( forName: .AVPlayerItemDidPlayToEndTime, object: avPlayer.currentItem, queue: .main) { [self] _ in Task { avPlayer.seek(to: .zero) avPlayer.play() } } return contentEntity } func generateHemisphere(radius: Float) -> MeshResource { logger.info("\(#function) \(#line)") var vertices: [SIMD3] = [] var indices: [UInt32] = [] var uvs: [SIMD2] = [] let segments = 36 let halfSegments = segments / 2 for latitude in 0...halfSegments { let theta = Float(latitude) * Float.pi / Float(halfSegments) let sinTheta = sin(theta) let cosTheta = cos(theta) for longitude in 0...segments / 2 { // Only cover 180 degrees let phi = Float(longitude) * Float.pi / Float(segments / 2) let sinPhi = sin(phi) let cosPhi = cos(phi) let x = radius * sinTheta * cosPhi let y = radius * cosTheta let z = radius * sinTheta * sinPhi vertices.append(SIMD3(x, y, z)) uvs.append(SIMD2(1.0 - Float(longitude) / Float(segments / 2), 1.0 - Float(latitude) / Float(halfSegments))) // Flip UVs horizontally } } for latitude in 0.. Bool { logger.info("\(#function) \(#line)") let type = "application/vnd.apple.mpegurl" var request = URLRequest(url: url) request.httpMethod = "HEAD" let (_, response) = try await URLSession.shared.data(for: request) if let httpResponse = response as? HTTPURLResponse { if let mimeType = httpResponse.allHeaderFields["Content-Type"] as? String { if mimeType.contains(type) { logger.info("\(#function) \(#line) mime type \(mimeType) found in url \(url.absoluteString)") return true } else { logger.error("\(#function) \(#line) mime type \(type) not found from url \(url.absoluteString)") return false } } else { logger.error("\(#function) \(#line) failed cannotParseResponse") throw URLError(.cannotParseResponse) } } else { logger.error("\(#function) \(#line) failed badServerResponse") throw URLError(.badServerResponse) } } func reset() { logger.info("\(#function) \(#line)") stop() removeReplayObserver() avPlayer.replaceCurrentItem(with: nil) } func removeReplayObserver() { logger.info("\(#function) \(#line)") NotificationCenter.default.removeObserver(self) } }