--- id: mobile-spatial-audio-video title: Spatial Audio / Video β€” AVFoundation / ExoPlayer category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [mobile, audio, video, spatial, vibe-coding] tech_stack: { language: "Swift / Kotlin", applicable_to: ["iOS", "Android"] } applied_in: [] aliases: [Spatial Audio, AirPods, Dolby Atmos, ExoPlayer, AVPlayer, HLS, DASH, picture-in-picture] --- # Spatial Audio / Video > Modern audio (3D / Atmos / Spatial). Modern video (HLS / DASH / PiP / AirPlay). iOS = AVFoundation. Android = ExoPlayer / Media3. ## πŸ“– 핡심 κ°œλ… - Spatial: head-tracked 3D audio (AirPods Pro / Max). - Atmos: object-based audio. - HLS / DASH: adaptive bitrate streaming. - PiP: Picture-in-Picture. ## πŸ’» μ½”λ“œ νŒ¨ν„΄ ### iOS β€” AVAudioPlayerNode (positional) ```swift import AVFoundation class SpatialAudio { let engine = AVAudioEngine() let player = AVAudioPlayerNode() let env = AVAudioEnvironmentNode() init() { engine.attach(player) engine.attach(env) // Listener at origin env.listenerPosition = AVAudio3DPoint(x: 0, y: 0, z: 0) env.listenerAngularOrientation = AVAudio3DAngularOrientation(yaw: 0, pitch: 0, roll: 0) // Source player.position = AVAudio3DPoint(x: 5, y: 0, z: -10) // 5m right, 10m forward // Connect engine.connect(player, to: env, format: nil) engine.connect(env, to: engine.mainMixerNode, format: env.outputFormat(forBus: 0)) try? engine.start() } func play(url: URL) { let file = try! AVAudioFile(forReading: url) player.scheduleFile(file, at: nil) player.play() } } ``` ### Spatial audio in AVPlayer (Atmos / Dolby) ```swift let player = AVPlayer(url: hlsUrl) // Spatial μžλ™ 적용 β€” iOS κ°€ hardware 검사 + apply. // Apple Music API κ°€ Atmos track ν‘œμ‹œ: let song = try await MusicCatalogResource(id: songId).response() let isAtmos = song.audioVariants.contains(.dolbyAtmos) ``` ### Head tracking (AirPods Pro/Max) ```swift import CoreMotion let manager = CMHeadphoneMotionManager() if manager.isDeviceMotionAvailable { manager.startDeviceMotionUpdates(to: .main) { motion, _ in let yaw = motion?.attitude.yaw ?? 0 let pitch = motion?.attitude.pitch ?? 0 // ν™˜κ²½ listener orientation update } } ``` β†’ Head κ°€ turn μ‹œ audio source μ •ν™• μœ„μΉ˜. ### Picture-in-Picture (iOS) ```swift import AVKit let pipController = AVPictureInPictureController(playerLayer: playerLayer) pipController.delegate = self pipController.startPictureInPicture() ``` ```swift // Info.plist // UIBackgroundModes: [audio] // λ˜λŠ” PiP 만 // UIBackgroundModes: [audio, "audio,picture-in-picture"] ``` ### HLS playback ```swift let url = URL(string: "https://example.com/video.m3u8")! let player = AVPlayer(url: url) let layer = AVPlayerLayer(player: player) view.layer.addSublayer(layer) player.play() // Quality control player.currentItem?.preferredPeakBitRate = 2_000_000 // 2 Mbps cap ``` ### AirPlay ```swift import MediaPlayer let routePicker = AVRoutePickerView(frame: .zero) view.addSubview(routePicker) // μ‚¬μš©μžκ°€ AirPlay device 선택 ``` β†’ iOS μžλ™. ### Background audio (iOS) ```swift // Capability: Background Modes β†’ Audio import AVFoundation let session = AVAudioSession.sharedInstance() try session.setCategory(.playback, mode: .default) try session.setActive(true) // Now Playing Info import MediaPlayer MPNowPlayingInfoCenter.default().nowPlayingInfo = [ MPMediaItemPropertyTitle: "Song Title", MPMediaItemPropertyArtist: "Artist", MPNowPlayingInfoPropertyElapsedPlaybackTime: 0, MPMediaItemPropertyPlaybackDuration: 240, ] // Remote commands (lock screen / control center) let cc = MPRemoteCommandCenter.shared() cc.playCommand.addTarget { _ in player.play(); return .success } cc.pauseCommand.addTarget { _ in player.pause(); return .success } cc.skipForwardCommand.addTarget { _ in /* +15s */; return .success } ``` ### Android β€” Media3 (modern ExoPlayer) ```kotlin implementation("androidx.media3:media3-exoplayer:1.4.0") implementation("androidx.media3:media3-ui:1.4.0") implementation("androidx.media3:media3-exoplayer-hls:1.4.0") ``` ```kotlin val player = ExoPlayer.Builder(context).build() val mediaItem = MediaItem.fromUri("https://example.com/video.m3u8") player.setMediaItem(mediaItem) player.prepare() player.play() // View playerView.player = player ``` ### Spatial audio (Android Auro / Atmos) ```kotlin val audioFormat = AudioFormat.Builder() .setEncoding(AudioFormat.ENCODING_E_AC3_JOC) // Atmos .setSampleRate(48000) .build() // μžλ™ β€” device κ°€ μ§€μ›ν•˜λ©΄ 적용 ``` β†’ Hardware-dependent. Pixel / Samsung 의 spatial. ### MediaSession (Android) ```kotlin val session = MediaSession.Builder(context, player) .setCallback(object : MediaSession.Callback { override fun onPlaybackResumption(session: MediaSession, controller: MediaSession.ControllerInfo): ListenableFuture { // ... } }) .build() // MediaNotification (background) class PlayerService : MediaSessionService() { private var session: MediaSession? = null override fun onCreate() { super.onCreate() session = MediaSession.Builder(this, player).build() } override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) = session } ``` β†’ Lock screen + notification controls. ### Picture-in-Picture (Android) ```xml ``` ```kotlin override fun onUserLeaveHint() { super.onUserLeaveHint() if (player.isPlaying) { enterPipMode() } } private fun enterPipMode() { val params = PictureInPictureParams.Builder() .setAspectRatio(Rational(16, 9)) .build() enterPictureInPictureMode(params) } ``` ### HLS (Android) ```kotlin val mediaItem = MediaItem.Builder() .setUri("https://example.com/video.m3u8") .setMimeType(MimeTypes.APPLICATION_M3U8) .build() val mediaSource = HlsMediaSource.Factory(DefaultHttpDataSource.Factory()).createMediaSource(mediaItem) player.setMediaSource(mediaSource) ``` ### DRM (premium content) ```kotlin val drmConfiguration = MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID) .setLicenseUri("https://license.example.com") .build() val mediaItem = MediaItem.Builder() .setUri(videoUri) .setDrmConfiguration(drmConfiguration) .build() ``` β†’ Netflix / Disney+ 같은. ### Adaptive bitrate ```kotlin val trackSelector = DefaultTrackSelector(context).apply { parameters = parameters.buildUpon() .setMaxVideoBitrate(2_000_000) // 2 Mbps cap .build() } ExoPlayer.Builder(context) .setTrackSelector(trackSelector) .build() ``` ### Captions / subtitles ```kotlin val mediaItem = MediaItem.Builder() .setUri(videoUri) .setSubtitleConfigurations(listOf( MediaItem.SubtitleConfiguration.Builder(subtitleUri) .setMimeType(MimeTypes.TEXT_VTT) .setLanguage("en") .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) .build() )) .build() ``` ### Streaming protocols ``` HLS (Apple): .m3u8 β€” iOS native, web ν˜Έν™˜ DASH: .mpd β€” Android / web WebRTC: Real-time (low latency) SRT: Live broadcast RTMP: Legacy (OBS β†’ server) β†’ HLS = κ°€μž₯ ν˜Έν™˜. DASH 도 일반. ``` ### Live streaming ``` LL-HLS (Low-Latency HLS): 1-3s WebRTC: < 500ms SRT: 100-500ms (broadcast quality) β†’ 라이브 video chat = WebRTC. Concert / sport = LL-HLS. ``` ### Encoding (server-side) ```bash # FFmpeg ffmpeg -i input.mp4 \ -c:v libx264 -preset fast \ -c:a aac \ -hls_time 4 \ -hls_list_size 0 \ -f hls output.m3u8 # Multiple bitrate (adaptive) ffmpeg -i input.mp4 \ -map 0:v -map 0:v -map 0:v \ -map 0:a -map 0:a -map 0:a \ -filter:v:0 scale=-2:1080 -b:v:0 5M \ -filter:v:1 scale=-2:720 -b:v:1 2.5M \ -filter:v:2 scale=-2:480 -b:v:2 1M \ -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2" \ -hls_time 4 -f hls output_%v.m3u8 ``` ### Audio routing (μ‚¬μš©μž device choice) ```swift // iOS let session = AVAudioSession.sharedInstance() // Override (speaker forced) try session.overrideOutputAudioPort(.speaker) // Bluetooth / AirPods μžλ™ try session.setCategory(.playAndRecord, options: [.allowBluetooth, .allowBluetoothA2DP]) ``` ### Audio focus (Android) ```kotlin val audioManager = getSystemService() val request = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN) .setAudioAttributes(AudioAttributesCompat.Builder() .setUsage(AudioAttributesCompat.USAGE_MEDIA) .setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC) .build()) .setOnAudioFocusChangeListener { focusChange -> when (focusChange) { AudioManager.AUDIOFOCUS_LOSS -> player.pause() AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> player.pause() AudioManager.AUDIOFOCUS_GAIN -> player.play() } } .build() AudioManagerCompat.requestAudioFocus(audioManager!!, request) ``` β†’ 톡화 μ‹œ μžλ™ pause λ“±. ### Cast / Chromecast ```kotlin implementation("androidx.media3:media3-cast:1.4.0") val castContext = CastContext.getSharedInstance(context) val castPlayer = CastPlayer(castContext) ``` ### YouTube / μ™ΈλΆ€ video ```ts // React Native import YoutubePlayer from 'react-native-youtube-iframe'; // Or webview ``` ### Media buttons / hardware controls ```swift // iOS β€” automatically MPRemoteCommandCenter // Android β€” MediaSession κ°€ μžλ™ ``` ### Performance ``` - 큰 video = adaptive bitrate - λ©”λͺ¨λ¦¬ β€” 1 player at a time - Background = MediaSession + foreground service (Android) - Hardware decode (h264 / h265 / AV1) ``` ### Test ```kotlin // Robolectric β€” Android val player = ExoPlayer.Builder(context).build() player.setMediaItem(MediaItem.fromUri(testUri)) player.prepare() shadowOf(Looper.getMainLooper()).idle() assertThat(player.isPlaying).isTrue() ``` ## πŸ€” μ˜μ‚¬κ²°μ • κΈ°μ€€ | μ‚¬μš© | μΆ”μ²œ | |---|---| | Music / podcast | AVAudioPlayer / ExoPlayer | | Video streaming | AVPlayer / Media3 ExoPlayer | | Spatial / 3D | AVAudioEnvironment | | Live | LL-HLS / WebRTC | | DRM | Widevine / FairPlay | | Background | MediaSession + capability | | PiP | λͺ¨λ‘ native API | ## ❌ μ•ˆν‹°νŒ¨ν„΄ - **Background audio + capability μ—†μŒ**: μ’…λ£Œ. - **MediaSession μ—†μŒ**: lock screen control X. - **Single bitrate**: 슬둜우 network 깨짐. - **Audio focus λ¬΄μ‹œ**: 톡화 μ‹œ μŒμ•… 계속. - **Memory leak (player release X)**: 큰 leak. - **HW decode μ•ˆ β€” software decode**: battery / λ°œμ—΄. - **Spatial κ°€μ • + non-spatial source**: hardware 이용 X. ## πŸ€– LLM ν™œμš© 힌트 - iOS = AVFoundation + AVPlayer. - Android = Media3 (modern ExoPlayer). - HLS adaptive bitrate. - MediaSession / NowPlayingInfo lock screen. - Spatial = AVAudioEnvironmentNode + head tracking. ## πŸ”— κ΄€λ ¨ λ¬Έμ„œ - [[Android_ExoPlayer_Patterns]] - [[iOS_Charts_Health]] - [[Web_WebRTC_Realtime]]