티스토리 뷰
라이브 스트리밍, 어떻게 동작할까?
유튜브, 트위치, 인스타그램 라이브 같은 서비스에서 실시간으로 방송을 송출하려면 RTMP(Real-Time Messaging Protocol)가 필요하다.
하지만 iOS 앱에서 직접 RTMP 스트리밍을 구현하려면 미디어 캡처, 인코딩, 전송 등의 과정이 필요하다.
이번 글에서는 iOS에서 RTMP 기반 라이브 스트리밍이 동작하는 과정과 실무 적용 방법을 알아보자!
iOS에서 RTMP 라이브 스트리밍의 기본 흐름
iOS에서 RTMP 스트리밍을 구현하려면 기본적으로 다음 4가지 단계가 필요하다.
1️⃣ 카메라 & 마이크 입력 받기
2️⃣ 비디오 & 오디오 데이터 인코딩
3️⃣ RTMP 서버로 실시간 전송
4️⃣ 뷰어(시청자)에서 재생하기
이제 각 단계별로 자세히 살펴보자!
1️⃣ 카메라 & 마이크 입력 받기
먼저, AVCaptureSession을 사용하여 카메라와 마이크에서 데이터를 가져온다.
import AVFoundation
class LiveStreamManager {
private let captureSession = AVCaptureSession()
private var videoOutput = AVCaptureVideoDataOutput()
private var audioOutput = AVCaptureAudioDataOutput()
func setupCaptureSession() {
captureSession.sessionPreset = .hd1280x720
// 🎥 카메라 입력 설정
guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front),
let videoInput = try? AVCaptureDeviceInput(device: camera) else { return }
captureSession.addInput(videoInput)
// 🎤 마이크 입력 설정
guard let microphone = AVCaptureDevice.default(for: .audio),
let audioInput = try? AVCaptureDeviceInput(device: microphone) else { return }
captureSession.addInput(audioInput)
// 📹 비디오 & 오디오 출력 추가
videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "videoQueue"))
audioOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "audioQueue"))
captureSession.addOutput(videoOutput)
captureSession.addOutput(audioOutput)
captureSession.startRunning()
}
}
📌 설명
- AVCaptureSession: 비디오 & 오디오 캡처 관리
- AVCaptureDeviceInput: 카메라 및 마이크 입력 설정
- AVCaptureVideoDataOutput, AVCaptureAudioDataOutput: 실시간 영상 & 오디오 데이터를 캡처
2️⃣ 비디오 & 오디오 인코딩
RTMP 서버로 데이터를 전송하려면 H.264 (비디오) / AAC (오디오) 코덱으로 압축해야 한다.
이 과정은 VideoToolbox와 AudioToolbox를 활용해 구현할 수 있다.
📹 비디오 인코딩 (H.264)
비디오 프레임을 H.264 포맷으로 변환해야 한다.
import VideoToolbox
extension LiveStreamManager: AVCaptureVideoDataOutputSampleBufferDelegate {
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
var encodedData: Data?
let status = VTCompressionSessionEncodeFrame(
compressionSession,
imageBuffer: pixelBuffer,
presentationTimeStamp: CMTimeMake(value: 0, timescale: 1),
duration: CMTime.invalid,
frameProperties: nil,
infoFlagsOut: nil,
outputHandler: { status, flags, sampleBuffer in
if status == noErr, let sampleBuffer = sampleBuffer {
encodedData = extractEncodedData(from: sampleBuffer)
}
}
)
if let data = encodedData {
sendToRTMPServer(data)
}
}
}
📌 설명
- VTCompressionSessionEncodeFrame(): H.264 인코딩
- CMSampleBufferGetImageBuffer(): 프레임을 픽셀 버퍼로 변환
- sendToRTMPServer(): 인코딩된 데이터를 RTMP 서버로 전송
🎤 오디오 인코딩 (AAC)
오디오 데이터는 AAC (Advanced Audio Codec)로 변환해야 한다.
import AudioToolbox
extension LiveStreamManager: AVCaptureAudioDataOutputSampleBufferDelegate {
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard let blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) else { return }
var audioData = Data()
CMBlockBufferCopyDataBytes(blockBuffer, atOffset: 0, dataLength: CMBlockBufferGetDataLength(blockBuffer), destination: &audioData)
sendToRTMPServer(audioData)
}
}
📌 설명
- CMSampleBufferGetDataBuffer(): 오디오 데이터 추출
- CMBlockBufferCopyDataBytes(): 오디오 데이터를 바이트 배열로 변환
3️⃣ RTMP 서버로 전송하기
이제 인코딩된 데이터를 RTMP 서버로 전송해야 한다.
RTMP는 FLV(Flexible Live Video) 포맷을 사용하여 데이터를 송출한다.
즉, H.264 비디오 + AAC 오디오 데이터를 RTMP 메시지로 패킹하여 서버에 전송해야 한다.
RTMP 스트리밍을 위해서는 라이브러리(LFLiveKit, HaishinKit) 를 사용할 수 있다.
✅ RTMP 연결 설정
import Network
class RTMPSocket {
private var connection: NWConnection?
func connect(to server: String, port: UInt16) {
connection = NWConnection(host: NWEndpoint.Host(server), port: NWEndpoint.Port(rawValue: port)!, using: .tcp)
connection?.stateUpdateHandler = { newState in
switch newState {
case .ready:
print("RTMP 서버 연결 성공")
case .failed(let error):
print("연결 실패: \(error)")
default:
break
}
}
connection?.start(queue: .global())
}
func sendData(_ data: Data) {
connection?.send(content: data, completion: .contentProcessed({ error in
if let error = error {
print("데이터 전송 실패: \(error)")
}
}))
}
}
📌 설명
- NWConnection을 사용해 RTMP 서버에 연결
- sendData(_:) 메서드로 인코딩된 H.264/AAC 데이터를 전송
✅ HaishinKit 라이브러리 활용 (추천)
HaishinKit은 Swift 기반의 RTMP 라이브러리로, 간단하게 RTMP 스트리밍을 구현할 수 있다.
📌 Podfile 추가
pod 'HaishinKit'
📌 RTMP 스트리밍 시작
import HaishinKit
class LiveStreamManager {
private let rtmpConnection = RTMPConnection()
private let rtmpStream: RTMPStream
init() {
rtmpStream = RTMPStream(connection: rtmpConnection)
}
func startStreaming() {
rtmpConnection.connect("rtmp://your-server/live")
rtmpStream.publish("stream-key")
}
}
📌 설명
- RTMPConnection(): RTMP 서버와 연결
- rtmpStream.publish("stream-key"): 스트리밍 시작
4️⃣ 시청자가 스트림을 볼 수 있도록 설정
RTMP 스트림을 송출하면, 클라이언트에서 이를 재생해야 한다.
시청자가 iOS 기기에서 스트림을 볼 수 있도록, AVPlayer를 사용하여 스트림을 재생하자.
import AVKit
class LiveStreamPlayerViewController: UIViewController {
private var player: AVPlayer?
override func viewDidLoad() {
super.viewDidLoad()
let streamURL = URL(string: "rtmp://your-server/live/stream-key")!
let playerItem = AVPlayerItem(url: streamURL)
player = AVPlayer(playerItem: playerItem)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = view.bounds
view.layer.addSublayer(playerLayer)
player?.play()
}
}
📌 설명
- AVPlayer를 사용해 RTMP 스트림을 재생
- AVPlayerItem(url:)을 통해 RTMP URL을 입력
- playerLayer를 추가하여 스트리밍 영상 출력
정리
iOS에서 RTMP 스트리밍을 직접 구현하려면 여러 단계가 필요하지만, 핵심 흐름은 다음과 같다.
1️⃣ AVCaptureSession을 사용해 카메라 & 마이크 데이터 캡처
2️⃣ VideoToolbox, AudioToolbox를 활용해 H.264 & AAC 인코딩
3️⃣ NWConnection을 사용해 RTMP 서버로 데이터 전송
4️⃣ AVPlayer를 사용해 iOS에서 스트리밍 영상 재생
이제 iOS 앱에서 직접 RTMP 라이브 스트리밍을 구현할 수 있다!
RTMP는 저지연 실시간 스트리밍에 적합하지만, WebRTC나 HLS 같은 대체 기술도 존재한다.
프로젝트에 따라 적절한 스트리밍 방식을 선택하는 것이 중요하며, RTMP는 특히 송출(입력) 용도로 강력한 선택지가 될 수 있다.
지금 바로 RTMP 서버를 설정하고, 직접 데이터를 송출하는 코드를 테스트해 보자
스스로 구축한 스트리밍 시스템을 통해 실시간 방송의 원리를 깊이 이해할 수 있을 것이다.
'영상 스트리밍' 카테고리의 다른 글
| 초저지연 실시간 스트리밍, WebRTC(Web Real-Time Communication) (1) | 2025.03.17 |
|---|---|
| 현대 스트리밍의 표준, HLS(HTTP Live Streaming) (0) | 2025.03.14 |
| 실시간 스트리밍의 핵심, RTMP(Real-Time Messaging Protocol) (0) | 2025.03.12 |