iOS端視頻直播技術

整個流媒體播放系統主要分爲視頻服務器端和iOS視頻播放器客戶端。
服務器端主要負責爲播放器端提供直播,點播等視頻流;
播放器端負責接收服務器發送的數據流,進行解碼和播放。

一、流媒體技術的含義
流媒體並不是一種新型的媒體,而是一種新的技術。廣義上的流媒體指的是使音頻和視頻形成穩定和連續的傳輸流和回放流的一系列技術、方法和協議的總稱,即流媒體技術;狹義上的流媒體是相對於傳統的下載-回放方式而言的,指的是一種從 Internet 上獲取音頻和視頻等多媒體數據的新方法,它能夠支持多媒體數據流的實時傳輸和實時播放。通過運用流媒體技術,服務器能夠向客戶機發送穩定和連續的多媒體數據流,客戶機在接收數據的同時以一個穩定的速率回放,而不用等數據全部下載完之後再進行回放。

二、HLS協議
HLS是HTTP Live Streaming的縮寫。它是蘋果公司實現的基於HTTP的流媒體傳輸協議,可以實現流媒體的直播和點播,也就是我們常說的Live和VOD。最先開始主要應用於iOS系統,爲iOS設備提供視頻直播和點播方案,現在在大多數的移動設備也實現了這個功能。HLS的點播,是將常見的分段HTTP點播,不同的是,他的分段非常小。實現的重點在於對媒體文件的分割,目前有很多開源工具。
相對於其他的流媒體直播協議,HLS最大的不同在於,客戶端獲取到的,並不是一個完整的數據流,而是一段一段的切片TS(MPEG-TS格式)。HLS協議在服務端將直播數據存儲爲連續的,一定時長的媒體文件,codec爲MPEG-TS,客戶端再按照playlist去在下載並播放這些文件,從而達到直播或者點播功能。HLS由於採取HTTP協議傳輸文件,所以不用考慮防火牆或者代理的問題,因爲一般的主機80端口應該是開放的。還有一個優點在於,客戶端可以很快的選擇和切換碼率,以適應不同帶寬條件下的播放。
HLS協議的實現過程:
首先對視頻數據進行錄入、編碼,然後服務器軟件的流分段程序將媒體視頻流分解成一系列簡短的.ts媒體文件,這些.ts文件被放置在web服務器上。這個流分段程序同時還創建一個索引文件,該索引文件包含元數據以及一個.m3u8媒體文件的列表,且索引文件的URL發佈到服務器上,客戶端軟件即可讀取索引,請求媒體文件,並將其在客戶端播放器中顯示出來。
根據以上的瞭解,想要實現HLS直播,需要研究並實現以下技術關鍵點:
採集視頻源和音頻源的數據
對原始數據進行H.264編碼和AAC編碼
視頻和音頻數據封裝爲MPEG-TS包
HLS分段生成策略及m3u8索引文件
HTTP傳輸協議

三、RTSP協議
與負責傳送數據的 RTP/RTCP不同,RTSP 主要負責在服務器和客戶端之間建立連接,並響應用戶的操作請求,如暫停,快進、快退、音量加減等。與HLS相比,RTSP傳輸的延遲更低。最常見的模式如下圖所示。

1787713-920525a5bf378ac6

 

四、播放器端解碼
FFmpeg簡介
FFmpeg 是一個跨平臺的開源視頻框架,能實現如視頻編碼、解碼、轉碼、串流、濾波、播放等豐富的功能。其支持的視頻格式以及播放協議非常豐富,幾乎包含了所有音視頻編解碼、封裝格式以及播放協議。而實時視頻直播一般使用的協議,如 RSTP,在 FFmpeg 中得到了很好的支持。 在手機直播軟件中,可以調用FFmpeg編寫一個播放器。

五、主要使用的協議:
HLS 協議 : >5M會被AppStore拒絕 服務器要求低 延遲高 多平臺
RTMP 協議: 電視直播 PC端使用 配合flash插件 及時性好
需要轉碼ffmpeg 延遲200ms
RTSP 協議: 攝像頭功能
軟解碼: ffmpeg
硬解碼:ios8之後 VideoToolBox 框架

直播過程大概爲5步
數據採集-->數據編碼--->數據傳輸-->數據解碼-->顯示到屏幕

數據採集:採集視頻及音頻數據。 原始數據。
數據編碼:編碼成(flv)
數據傳輸:
(推流)把你本地得到編碼數據。上傳到流媒體服務器。rtmp hls (rtmp協議)
(拉流) 把你的數據從流媒體服務器上拉下來。
數據解碼: 流。。音頻流(),視頻流。。
播放顯示:openGLES:渲染。。GPUImage

視頻編碼

1、1>初始化視頻編碼類

初始化調用:
VTCompressionSessionCreate(
kCFAllocatorDefault,
width,
height,
kCMVideoCodecType_H264,
nil,
attributes as CFDictionary?,
nil,
callback,
Unmanaged.passUnretained(self).toOpaque(),
&_session)
需要設置下,幅面、碼率、幀率、回調函數等常規信息。
width,height分別是編碼的幅面大小。
kCMVideoCodecType_H264 採用的編碼技術。
attributes 流設置,這裏面涉及到的參數:
[kVTCompressionPropertyKey_RealTime: kCFBooleanTrue, // 實時編碼
kVTCompressionPropertyKey_ProfileLevel: kVTProfileLevel_H264_Baseline_3_1 as NSObject, //編碼畫質 低清Baseline Level 1.3,標清Baseline Level 3,半高清Baseline Level 3.1,全高清Baseline Level 4.1(BaseLine表示直播,Main存儲媒體,Hight高清存儲【只有:3.1 & 4.1】)
kVTCompressionPropertyKey_AverageBitRate: Int(bitrate) as NSObject, // 設置碼率
kVTCompressionPropertyKey_ExpectedFrameRate: NSNumber(value: expectedFPS), // 設置幀率
kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration: NSNumber(value: 2.0) // 關鍵幀間隔,單位秒, kVTCompressionPropertyKey_AllowFrameReordering: !isBaseline as NSObject, //是否產生B幀,直播設置爲false【B幀是雙向差別幀,也就是B幀記錄的是本幀與前後幀的差別,B幀可以大大減少空間,但運算量較大】
kVTCompressionPropertyKey_PixelTransferProperties: [
"ScalingMode": "Trim"
] as NSObject] 像素轉換規則
kVTCompressionPropertyKey_H264EntropyMode:kVTH264EntropyMode_CABAC // 如果是264編碼指定算法

2、2設置回調函數。

private var callback: VTCompressionOutputCallback = {(
outputCallbackRef: UnsafeMutableRawPointer?,
sourceFrameRef: UnsafeMutableRawPointer?,
status: OSStatus,
infoFlags: VTEncodeInfoFlags,
sampleBuffer: CMSampleBuffer?) in
guard let ref: UnsafeMutableRawPointer = outputCallbackRef,
let sampleBuffer: CMSampleBuffer = sampleBuffer, status == noErr else {
return
}
let encoder: H264Encoder = Unmanaged<H264Encoder>.fromOpaque(ref).takeUnretainedValue() //因爲初始化的時候傳了進去,現在取回來。
encoder.formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer) // 得到視頻流,用於編碼
encoder.delegate?.sampleOutput(video: sampleBuffer) //交給外部處理,通過解析 CMSampleBufferRef 分別處理SPS,PPS,I-Frame和非I-Frame,然後通過RTMP推出去。
}

2.3 編碼

編碼後會自動調用2.2的回調函數。
BTW:這是在視頻採集的時候調用這個
func captureOutput(_ captureOutput: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
VTCompressionSessionEncodeFrame(
session,
sampleBuffer,
CMSampleBufferGetPresentationTimeStamp(sampleBuffer),
CMSampleBufferGetDuration(sampleBuffer),
nil,
nil,
&flags
)
}

這個就是CMSampleBuffer的內部結構圖,編碼和解碼前後的內部結構
編碼就是CVPixelBuffer—>CMSampleBufferRef,解碼反之。

 


2、音頻編碼

2、1創建編碼器

AudioConverterNewSpecific(
&inSourceFormat!, //輸入參數
&inDestinationFormat, //輸出參數
UInt32(inClassDescriptions.count), //音頻描述符數量
&inClassDescriptions, //音頻描述符數組
&converter //編碼器
)
創建好編碼器後,還要修改一下編碼器的碼率
UInt32 outputBitrate = 64000 * channelscount // 還要* 通道數。需要注意,AAC並不是隨便的碼率都可以支持。比如,如果PCM採樣率是44100KHz,那麼碼率可以設置64000bps,如果是16K,可以設置爲32000bps。
UInt32 propSize = sizeof(outputBitrate);
AudioConverterSetProperty(audioConverter,
kAudioConverterEncodeBitRate,
propSize,
&outputBitrate);

2、2音頻描述文件

inDestinationFormat = AudioStreamBasicDescription()
inDestinationFormat!.mSampleRate = sampleRate == 0 ? inSourceFormat!.mSampleRate : sampleRate //設置採樣率,有 32K, 44.1K,48K
inDestinationFormat!.mFormatID = kAudioFormatMPEG4AAC // 採用AAC編碼方式
inDestinationFormat!.mFormatFlags = profile //指明格式的細節. 設置爲 0 說明沒有子格式。
inDestinationFormat!.mBytesPerPacket = 0 //每個音頻包的字節數,該字段設置爲 0, 表明包裏的字節數是變化的。
inDestinationFormat!.mFramesPerPacket = 1024 每個音頻包幀的數量. 對於未壓縮的數據設置爲 1. 動態碼率格式,這個值是一個較大的固定數字,比如說AAC的1024。如果是動態幀數(比如Ogg格式)設置爲0。
inDestinationFormat!.mBytesPerFrame = 0 // 每個幀的字節數。對於壓縮數據,設置爲 0.
inDestinationFormat!.mChannelsPerFrame = 1 //音頻聲道數
inDestinationFormat!.mBitsPerChannel = 0 // 壓縮數據,該值設置爲0.
inDestinationFormat!.mReserved = 0 // 用於字節對齊,必須是0.
CMAudioFormatDescriptionCreate(
kCFAllocatorDefault, &inDestinationFormat!, 0, nil, 0, nil, nil, &formatDescription
)

2、3轉碼

通過音頻捕獲獲取音頻流
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
// 編碼流程:
首先,創建一個 AudioBufferList,並將輸入數據存到 AudioBufferList裏。
其次,設置輸出。
然後,調用 AudioConverterFillComplexBuffer 方法,該方法又會調用 inInputDataProc 回調函數,將輸入數據拷貝到編碼器中。
最後,轉碼。將轉碼後的數據輸出到指定的輸出變量中。
//設置輸入
var blockBuffer: CMBlockBuffer?
currentBufferList = AudioBufferList.allocate(maximumBuffers: 1)
CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(
sampleBuffer,
nil,
currentBufferList!.unsafeMutablePointer,
AudioBufferList.sizeInBytes(maximumBuffers: 1),
kCFAllocatorDefault,
kCFAllocatorDefault,
0,
&blockBuffer
)
// 設置輸出
var finished: Bool = false
while !finished {
var ioOutputDataPacketSize: UInt32 = 1
let dataLength: Int = blockBuffer!.dataLength
let outOutputData: UnsafeMutableAudioBufferListPointer = AudioBufferList.allocate(maximumBuffers: 1)
outOutputData[0].mNumberChannels = inDestinationFormat.mChannelsPerFrame
outOutputData[0].mDataByteSize = UInt32(dataLength)
outOutputData[0].mData = UnsafeMutableRawPointer.allocate(byteCount: dataLength, alignment: 0)
let status: OSStatus = AudioConverterFillComplexBuffer(
converter,
inputDataProc,
Unmanaged.passUnretained(self).toOpaque(),
&ioOutputDataPacketSize,
outOutputData.unsafeMutablePointer,
nil
)
if 0 <= status && ioOutputDataPacketSize == 1 {
var result: CMSampleBuffer?
var timing: CMSampleTimingInfo = CMSampleTimingInfo(sampleBuffer: sampleBuffer)
let numSamples: CMItemCount = sampleBuffer.numSamples
CMSampleBufferCreate(kCFAllocatorDefault, nil, false, nil, nil, formatDescription, numSamples, 1, &timing, 0, nil, &result)
CMSampleBufferSetDataBufferFromAudioBufferList(result!, kCFAllocatorDefault, kCFAllocatorDefault, 0, outOutputData.unsafePointer) // 這裏通過fillComplexBuffer指向outOutputData,然後通過inputDataProc回調,最後再次回調給自己的onInputDataForAudioConverter函數,再通過memcpy拷貝到這個outOutputData裏。下面的這行代碼才最終把buffer數據拿走
delegate?.sampleOutput(audio: result!)
} else {
finished = true
}
for i in 0..<outOutputData.count {
free(outOutputData[i].mData)
}
free(outOutputData.unsafeMutablePointer)
}
}
// 編碼解釋
AudioConverterFillComplexBuffer(
inAudioConverter: AudioConverterRef,
inInputDataProc: AudioConverterComplexInputDataProc,
inInputDataProcUserData: UnsafeMutablePointer,
ioOutputDataPacketSize: UnsafeMutablePointer<UInt32>,
outOutputData: UnsafeMutablePointer<AudioBufferList>,
outPacketDescription: AudioStreamPacketDescription
) -> OSStatus
inAudioConverter : 轉碼器
inInputDataProc : 回調函數。用於將PCM數據餵給編碼器。
inInputDataProcUserData : 用戶自定義數據指針。
ioOutputDataPacketSize : 輸出數據包大小。
outOutputData : 輸出數據 AudioBufferList 指針。
outPacketDescription : 輸出包描述符。
回調處理
private var inputDataProc: AudioConverterComplexInputDataProc = {(
converter: AudioConverterRef,
ioNumberDataPackets: UnsafeMutablePointer<UInt32>,
ioData: UnsafeMutablePointer<AudioBufferList>,
outDataPacketDescription: UnsafeMutablePointer<UnsafeMutablePointer<AudioStreamPacketDescription>?>?,
inUserData: UnsafeMutableRawPointer?) in
return Unmanaged<AACEncoder>.fromOpaque(inUserData!).takeUnretainedValue().onInputDataForAudioConverter(
ioNumberDataPackets,
ioData: ioData,
outDataPacketDescription: outDataPacketDescription
)
}
再回調處理
func onInputDataForAudioConverter(
_ ioNumberDataPackets: UnsafeMutablePointer<UInt32>,
ioData: UnsafeMutablePointer<AudioBufferList>,
outDataPacketDescription: UnsafeMutablePointer<UnsafeMutablePointer<AudioStreamPacketDescription>?>?) -> OSStatus {
guard let bufferList: UnsafeMutableAudioBufferListPointer = currentBufferList else {
ioNumberDataPackets.pointee = 0
return -1
}
memcpy(ioData, bufferList.unsafePointer, bufferListSize) // 通過上面的回調傳值處理,然後再這裏在通過memcpy把數據拷貝到iodata裏實現數據的保存到outOutputData
ioNumberDataPackets.pointee = 1
free(bufferList.unsafeMutablePointer)
currentBufferList = nil
return noErr
}


3. 流合成。

通過1、2的音視頻的編碼操作,下面我們就可以合成流以便給Socket準備發送的數據

3.1 視頻合成流
func sampleOutput(video sampleBuffer: CMSampleBuffer) {
let keyframe: Bool = !sampleBuffer.dependsOnOthers
var compositionTime: Int32 = 0
let presentationTimeStamp: CMTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
var decodeTimeStamp: CMTime = CMSampleBufferGetDecodeTimeStamp(sampleBuffer)
if decodeTimeStamp == kCMTimeInvalid {
decodeTimeStamp = presentationTimeStamp
} else {
compositionTime = Int32((decodeTimeStamp.seconds - decodeTimeStamp.seconds) * 1000)
}
let delta: Double = (videoTimestamp == kCMTimeZero ? 0 : decodeTimeStamp.seconds - videoTimestamp.seconds) * 1000
guard let data: Data = sampleBuffer.dataBuffer?.data, 0 <= delta else {
return
}
var buffer: Data = Data([((keyframe ? FLVFrameType.key.rawValue : FLVFrameType.inter.rawValue) << 4) | FLVVideoCodec.avc.rawValue, FLVAVCPacketType.nal.rawValue]) // 設置頭
buffer.append(contentsOf: compositionTime.bigEndian.data[1..<4]) // 大小端處理
buffer.append(data) //添加流數據
delegate?.sampleOutput(video: buffer, withTimestamp: delta, muxer: self) //回調出去
videoTimestamp = decodeTimeStamp
}
public enum FLVFrameType: UInt8 {
case key = 1
3.1 視頻合成流
func sampleOutput(video sampleBuffer: CMSampleBuffer) {
let keyframe: Bool = !sampleBuffer.dependsOnOthers
var compositionTime: Int32 = 0
let presentationTimeStamp: CMTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
var decodeTimeStamp: CMTime = CMSampleBufferGetDecodeTimeStamp(sampleBuffer)
if decodeTimeStamp == kCMTimeInvalid {
decodeTimeStamp = presentationTimeStamp
} else {
compositionTime = Int32((decodeTimeStamp.seconds - decodeTimeStamp.seconds) * 1000)
}
let delta: Double = (videoTimestamp == kCMTimeZero ? 0 : decodeTimeStamp.seconds - videoTimestamp.seconds) * 1000
guard let data: Data = sampleBuffer.dataBuffer?.data, 0 <= delta else {
return
}
var buffer: Data = Data([((keyframe ? FLVFrameType.key.rawValue : FLVFrameType.inter.rawValue) << 4) | FLVVideoCodec.avc.rawValue, FLVAVCPacketType.nal.rawValue]) // 設置頭
buffer.append(contentsOf: compositionTime.bigEndian.data[1..<4]) // 大小端處理
buffer.append(data) //添加流數據
delegate?.sampleOutput(video: buffer, withTimestamp: delta, muxer: self) //回調出去
videoTimestamp = decodeTimeStamp
}
public enum FLVFrameType: UInt8 {
case key = 1
case inter = 2
case disposable = 3
case generated = 4
case command = 5
}

3、2音頻合成流

func sampleOutput(audio sampleBuffer: CMSampleBuffer) {
let presentationTimeStamp: CMTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
let delta: Double = (audioTimestamp == kCMTimeZero ? 0 : presentationTimeStamp.seconds - audioTimestamp.seconds) * 1000
guard let data: Data = sampleBuffer.dataBuffer?.data, 0 <= delta else {
return
}
var buffer: Data = Data([RTMPMuxer.aac, FLVAACPacketType.raw.rawValue]) // 設置頭
buffer.append(data) // 添加流數據
delegate?.sampleOutput(audio: buffer, withTimestamp: delta, muxer: self) // 回調出去
audioTimestamp = presentationTimeStamp
}
public enum FLVAACPacketType: UInt8 {
case seq = 0
case raw = 1
}

3、3組RTMP協議數據,僅供參考
func sampleOutput(audio buffer: Data, withTimestamp: Double, muxer: RTMPMuxer) {
guard readyState == .publishing else {
return
}
let type: FLVTagType = .audio
let length: Int = rtmpConnection.socket.doOutput(chunk: // 發送數據給socket,寫入inputstream
RTMPChunk( //拼接流數據
type: audioWasSent ? .one : .zero, // 是否是第一次發送用於處理大小端數據
streamId: type.streamId,
message: RTMPAudioMessage(streamId: id, timestamp: UInt32(audioTimestamp), payload: buffer)), locked: nil)
audioWasSent = true
OSAtomicAdd64(Int64(length), &info.byteCount) 原子鎖定,避免重複添加。發送數據大小統計
audioTimestamp = withTimestamp + (audioTimestamp - floor(audioTimestamp))
}
和上面很接近只是增加了鎖
func sampleOutput(video buffer: Data, withTimestamp: Double, muxer: RTMPMuxer) {
guard readyState == .publishing else {
return
}
let type: FLVTagType = .video
OSAtomicOr32Barrier(1, &mixer.videoIO.encoder.locked)
let length: Int = rtmpConnection.socket.doOutput(chunk: RTMPChunk(
type: videoWasSent ? .one : .zero,
streamId: type.streamId,
message: RTMPVideoMessage(streamId: id, timestamp: UInt32(videoTimestamp), payload: buffer)
), locked: &mixer.videoIO.encoder.locked)
videoWasSent = true
OSAtomicAdd64(Int64(length), &info.byteCount)
videoTimestamp = withTimestamp + (videoTimestamp - floor(videoTimestamp))
frameCount += 1
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章