이 글은 누구를 위한 것인가
- OTT, 교육, 라이브 스트리밍 앱을 구현하려는 팀
- HLS 스트리밍과 DRM 보호를 적용하려는 개발자
- 오프라인 다운로드 기능을 구현하려는 팀
들어가며
HLS(HTTP Live Streaming)는 Apple이 개발한 적응형 비트레이트 스트리밍 프로토콜이다. 네트워크 상황에 따라 화질을 자동으로 조정하고, DRM으로 콘텐츠를 보호한다. iOS는 AVPlayer, Android는 ExoPlayer(Media3)가 표준이다.
이 글은 bluefoxdev.kr의 HLS 동영상 스트리밍 구현 가이드 를 참고하여 작성했습니다.
1. HLS 스트리밍 아키텍처
[HLS 구조]
Master Playlist (.m3u8)
├── 1080p Stream → 세그먼트들 (.ts/.fmp4)
├── 720p Stream
├── 480p Stream
└── 오디오 스트림
[ABR (Adaptive Bitrate)]
네트워크 대역폭 측정
→ 적절한 화질 자동 선택
→ 버퍼링 없이 전환
[DRM]
iOS: FairPlay Streaming (FPS)
Android: Widevine
Web: PlayReady
크로스 플랫폼: MPEG-DASH + Common Encryption
[CDN 구성]
원본: S3 + CloudFront
세그먼트 캐싱: CDN 엣지
지역별 배포: 지연 시간 최소화
[오프라인 다운로드]
iOS: AVAssetDownloadTask
Android: ExoPlayer DownloadManager
암호화: 기기별 키로 로컬 암호화
2. AVPlayer 구현
import AVKit
import SwiftUI
import Combine
class VideoPlayerViewModel: ObservableObject {
@Published var isPlaying = false
@Published var currentTime: Double = 0
@Published var duration: Double = 0
@Published var isLoading = true
@Published var selectedQuality: VideoQuality = .auto
var player: AVPlayer?
private var timeObserver: Any?
private var cancellables = Set<AnyCancellable>()
enum VideoQuality: String, CaseIterable {
case auto = "자동"
case hd1080 = "1080p"
case hd720 = "720p"
case sd480 = "480p"
}
func setupPlayer(urlString: String) {
guard let url = URL(string: urlString) else { return }
// HLS 에셋
let asset = AVURLAsset(url: url)
let playerItem = AVPlayerItem(asset: asset)
// 버퍼 설정
playerItem.preferredForwardBufferDuration = 10 // 10초 선버퍼
player = AVPlayer(playerItem: playerItem)
// 로딩 상태 관찰
playerItem.publisher(for: \.status)
.receive(on: DispatchQueue.main)
.sink { [weak self] status in
self?.isLoading = status != .readyToPlay
}
.store(in: &cancellables)
// 재생 시간 관찰
timeObserver = player?.addPeriodicTimeObserver(
forInterval: CMTime(seconds: 0.5, preferredTimescale: 600),
queue: .main
) { [weak self] time in
self?.currentTime = time.seconds
self?.duration = self?.player?.currentItem?.duration.seconds ?? 0
}
// 끝 감지
NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime, object: playerItem)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.isPlaying = false
self?.player?.seek(to: .zero)
}
.store(in: &cancellables)
}
func togglePlayPause() {
if isPlaying { player?.pause() } else { player?.play() }
isPlaying.toggle()
}
func seek(to seconds: Double) {
let time = CMTime(seconds: seconds, preferredTimescale: 600)
player?.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero)
}
// 오프라인 다운로드
func downloadForOffline(urlString: String, assetTitle: String) async throws {
guard let url = URL(string: urlString) else { return }
let asset = AVURLAsset(url: url)
let downloadTask = AVAssetDownloadURLSession.shared.makeAssetDownloadTask(
asset: asset,
assetTitle: assetTitle,
assetArtworkData: nil,
options: [AVAssetDownloadTaskMinimumRequiredMediaBitrateKey: 2_000_000]
)
downloadTask?.resume()
}
deinit {
if let observer = timeObserver { player?.removeTimeObserver(observer) }
}
}
// SwiftUI 비디오 플레이어 뷰
struct CustomVideoPlayer: View {
@StateObject private var viewModel = VideoPlayerViewModel()
let url: String
var body: some View {
ZStack {
VideoPlayer(player: viewModel.player)
.ignoresSafeArea()
if viewModel.isLoading {
ProgressView().tint(.white)
}
// 커스텀 컨트롤 오버레이
VStack {
Spacer()
HStack {
Button(action: viewModel.togglePlayPause) {
Image(systemName: viewModel.isPlaying ? "pause.fill" : "play.fill")
.foregroundColor(.white).font(.title)
}
Slider(
value: Binding(get: { viewModel.currentTime }, set: viewModel.seek),
in: 0...max(viewModel.duration, 1)
)
Text(formatTime(viewModel.currentTime))
.foregroundColor(.white).font(.caption)
}
.padding()
.background(.black.opacity(0.4))
}
}
.onAppear { viewModel.setupPlayer(urlString: url) }
}
private func formatTime(_ seconds: Double) -> String {
let m = Int(seconds) / 60
let s = Int(seconds) % 60
return String(format: "%d:%02d", m, s)
}
}
마무리
HLS의 핵심은 ABR(Adaptive Bitrate)이다. 네트워크 대역폭에 따라 화질이 자동으로 전환되어 버퍼링을 최소화한다. DRM 보호는 FairPlay(iOS)/Widevine(Android) 각각 구현이 필요하며, AVAssetDownloadTask로 오프라인 다운로드를 지원하면 비행기 모드에서도 재생이 가능하다.