公司的直播項(xiàng)目一直采用的網(wǎng)易云的直播SDK听隐,后來(lái)產(chǎn)品需求需要我們使用手機(jī)端作為音視頻采集設(shè)備沪么,使用電視端作為傳輸與顯示媒介,把視頻數(shù)據(jù)展示在電視和Windows端,音頻只展示W(wǎng)indows端,這樣一個(gè)需求,這樣我這邊需要做到的:
1.音視頻采集已經(jīng)考慮的傳輸數(shù)據(jù)大小問(wèn)題需要對(duì)音視頻進(jìn)行壓縮編碼
2.數(shù)據(jù)傳輸惜索,因?yàn)殡娨暫褪謾C(jī)在同一個(gè)局域網(wǎng)所以,我們使用socket進(jìn)行數(shù)據(jù)傳輸
以上兩點(diǎn)看似簡(jiǎn)單其實(shí)里面內(nèi)容挺多堤如,各種問(wèn)題的出現(xiàn)讓我在這個(gè)項(xiàng)目中跌了很多跟頭榔至,對(duì)于遇到的問(wèn)題做以下記錄
音視頻采集
iOS這邊處理音視頻采集并不難彩匕,網(wǎng)上教程一大把,特別是視頻采集很簡(jiǎn)單宙攻,只需有一點(diǎn)要注意就是像素的大小設(shè)置溢陪,蘋果有兩種方案:一種是你自定義畫布大小邓馒,第二個(gè)你使用硬件支持的大小
自定義大小
pixW = 320;pixH = 240; //畫布寬高自定義
self.captureSession.sessionPreset = AVCaptureSessionPresetHigh; //畫質(zhì) 采用最高 數(shù)據(jù)也比較大
使用硬件支持
這個(gè)需要你使用系統(tǒng)像素采集的寬高
if ([self.captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) { //判斷當(dāng)前采集的設(shè)備 是否支持 這個(gè)像素
self.captureSession.sessionPreset = AVCaptureSessionPreset1280x720;
音頻這邊的就有多個(gè)選擇, 比如:AVCaptureDevice, AudioQueue以及Audio Unit。其中 Audio Unit是最底層的接口胆萧,它的優(yōu)點(diǎn)是功能強(qiáng)大羹唠,延遲低; 而缺點(diǎn)是學(xué)習(xí)成本高,難度大萌焰。對(duì)于一般的iOS應(yīng)用程序,AVCaptureDevice和AudioQueue完全夠用了掌猛。但對(duì)于音視頻直播辣往,最好還是使用 Audio Unit 進(jìn)行處理许起,這樣可以達(dá)到最佳的效果猛频。
因?yàn)槲疫@邊做的是直播的所以為了追求低延遲,最好的效果越锈,就采用了Audio Unit進(jìn)行處理,因?yàn)槎际荂語(yǔ)言的代碼而且還要和OC進(jìn)行結(jié)合所以學(xué)習(xí)起來(lái)確實(shí)很費(fèi)勁 所以對(duì)里面的關(guān)鍵代碼做了注釋
// AVAudioSession的接口比較簡(jiǎn)單膘滨。APP啟動(dòng)的時(shí)候會(huì)自動(dòng)幫激活A(yù)VAudioSession甘凭,我們可以手動(dòng) 設(shè)置 開(kāi)始的時(shí)候b可以播放也可以錄音
AVAudioSession* session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker error:&error];
[session setActive:YES error:nil];
//啟用錄音功能
UInt32 inputEnableFlag = 1;
CheckError(AudioUnitSetProperty(_remoteIOUnit,
kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Input,
1,
&inputEnableFlag,
sizeof(inputEnableFlag)),
"Open input of bus 1 failed");
// Open output of bus 0(output speaker)//禁用播放功能
UInt32 outputEnableFlag = 1;
CheckError(AudioUnitSetProperty(_remoteIOUnit,
kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Output,
0,
&outputEnableFlag,
sizeof(outputEnableFlag)),
"Open output of bus 0 failed");
//Set up stream format for input and output
/**
AudioStreamBasicDescription:
mSampleRate; 采樣率, eg. 44100
mFormatID; 格式, eg. kAudioFormatLinearPCM
mFormatFlags; 標(biāo)簽格式, eg. kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked
mBytesPerPacket; 每個(gè)Packet的Bytes數(shù)量, eg. 2
mFramesPerPacket; 每個(gè)Packet的幀數(shù)量, eg. 1
mBytesPerFrame; (mBitsPerChannel / 8 * mChannelsPerFrame) 每幀的Byte數(shù), eg. 2
mChannelsPerFrame; 1:單聲道;2:立體聲, eg. 1
mBitsPerChannel; 語(yǔ)音每采樣點(diǎn)占用位數(shù)[8/16/24/32], eg. 16
mReserved; 保留
*/
_streamFormat.mFormatID = kAudioFormatLinearPCM;
_streamFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
_streamFormat.mSampleRate = kRate;
_streamFormat.mFramesPerPacket = 1;
_streamFormat.mBytesPerFrame = 2;
_streamFormat.mBytesPerPacket = 2;
_streamFormat.mBitsPerChannel = kBits;
_streamFormat.mChannelsPerFrame = kChannels;
CheckError(AudioUnitSetProperty(_remoteIOUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
0,
&_streamFormat,
sizeof(_streamFormat)),
"kAudioUnitProperty_StreamFormat of bus 0 failed");
CheckError(AudioUnitSetProperty(_remoteIOUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output,
1,
&_streamFormat,
sizeof(_streamFormat)),
"kAudioUnitProperty_StreamFormat of bus 1 failed");
//音頻采集結(jié)果回調(diào)
AURenderCallbackStruct recordCallback;
recordCallback.inputProc = recordCallback_xb;
recordCallback.inputProcRefCon = (__bridge void *)(self);
CheckError(AudioUnitSetProperty(_remoteIOUnit,
kAudioOutputUnitProperty_SetInputCallback,
kAudioUnitScope_Output,
1,
&recordCallback,
sizeof(recordCallback)),
"couldnt set remote i/o render callback for output");
音視頻編碼
視頻我采用的是常用的H264編碼火邓,之前說(shuō)了視頻采集很簡(jiǎn)單丹弱,但視頻264編碼也是C語(yǔ)言寫里面的設(shè)置還是有很多需要注意的
OSStatus status = VTCompressionSessionCreate(NULL,//分配器,如果使用NULL的話,就使用默認(rèn)的. CFAllocatorRef _Nullable allocator
width,//視頻幀的象素寬 int32_t width
height,//視頻幀的象素高 int32_t height
kCMVideoCodecType_H264,//編碼器的類型 CMVideoCodecType codecType
NULL,//如果用指定的視頻編碼器,就要設(shè)置這個(gè).使用NULL就是videoToolbox自己選擇一個(gè) CFDictionaryRef _Nullable encoderSpecification
NULL,//元象素緩存,如果你不要videoToolbox給你創(chuàng)建,就傳NULL.使用非VTB分配的緩存,可以讓你有機(jī)會(huì)拷貝圖片 CFDictionaryRef _Nullable sourceImageBufferAttributes,
NULL,//壓縮數(shù)據(jù)分配器.傳NULL可以使用默認(rèn)的. CFAllocatorRef _Nullable compressedDataAllocator
didCompressH264,//回調(diào),這個(gè)方法會(huì)在另一個(gè)線程上被異步的VTCompressionSessionEncodeFrame調(diào)用.只有在你要使VTCompressionSessionEncodeFrameWithOutputHandler去編碼幀時(shí),才可以設(shè)置為NULL. VTCompressionOutputCallback _Nullable outputCallback
(__bridge void *)(self),//回調(diào)方法所在的實(shí)例,回調(diào)方法是全局的可以設(shè)置為NULL void * _Nullable outputCallbackRefCon
&EncodingSession);//用來(lái)接收新的compression session VTCompressionSessionRef _Nullable * _Nonnull compressionSessionOut
NSLog(@"H264: VTCompressionSessionCreate %d", (int)status);
// Set the properties
VTSessionSetProperty(EncodingSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);//實(shí)時(shí)運(yùn)行
VTSessionSetProperty(EncodingSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);
//設(shè)置碼率德撬,均值,單位是byte
SInt32 bitRate = width*height*3 * 4 * 8 * 2; //越高效果越屌 幀數(shù)據(jù)越大
CFNumberRef ref = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
VTSessionSetProperty(EncodingSession, kVTCompressionPropertyKey_AverageBitRate, ref);
CFRelease(ref);
//設(shè)置碼率上限躲胳,均值蜓洪,單位是byte
int bitRateLimit = width * height * 3 * 4 * 2;
CFNumberRef bitRateLimitRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRateLimit);
VTSessionSetProperty(EncodingSession, kVTCompressionPropertyKey_DataRateLimits, bitRateLimitRef);
int frameInterval = 10; //關(guān)鍵幀間隔 越低效果越屌 幀數(shù)據(jù)越大
CFNumberRef frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
VTSessionSetProperty(EncodingSession, kVTCompressionPropertyKey_MaxKeyFrameInterval,frameIntervalRef);
CFRelease(frameIntervalRef);
設(shè)置碼率這個(gè)一定要 把寬高數(shù)據(jù)放大100倍左右不然圖像會(huì)很模糊,當(dāng)時(shí)因?yàn)檫@個(gè)問(wèn)題困擾了我3天坯苹,其他倒是沒(méi)有太大問(wèn)題的
Socket 數(shù)據(jù)傳輸
socket我采用的是GCDAsyncSocket 這個(gè)框架,使用方法也很簡(jiǎn)單網(wǎng)上教程很多隆檀,在這不再詳述,只是記錄一下注意的問(wèn)題
比如數(shù)據(jù)傳輸 常量數(shù)據(jù)進(jìn)行大小端轉(zhuǎn)換問(wèn)題 粹湃,還有因?yàn)閿?shù)據(jù)都是采用二級(jí)制進(jìn)行發(fā)送所以要對(duì)數(shù)據(jù)進(jìn)行組合恐仑,但是音視頻發(fā)送數(shù)據(jù)要單獨(dú)寫一個(gè)方法,不然NSMutableData 對(duì)象會(huì)造成內(nèi)存泄露無(wú)法釋放为鳄,原因是視頻采集的代理方法執(zhí)行速度太快導(dǎo)致此對(duì)象釋放不及時(shí)
char sign[8] = "KOCLACMD";
uint16_t data_type = 0x02;
uint16_t media_type = 0x06;
uint32_t width = pixW;
uint32_t height = pixH;
uint32_t len = (uint32_t)data.length;
// 字節(jié)序轉(zhuǎn)換 小端模式轉(zhuǎn)換大端 匹配網(wǎng)絡(luò)字節(jié)序
HTONS(data_type);
HTONS(media_type);
HTONL(len);
HTONL(width);
HTONL(height);
NSMutableData *muta = [NSMutableData data];
[muta appendBytes:&sign length:8];
[muta appendBytes:&data_type length:2];
[muta appendBytes:&media_type length:2];
[muta appendBytes:&len length:4];
[muta appendBytes:&width length:4];
[muta appendBytes:&height length:4];
[muta appendData:data];
// NSLog(@"Video data (%lu): %@", (unsigned long)muta.length, muta.description);
[_clientSocket writeData:muta withTimeout:-1 tag:0];
因?yàn)槟銈鬏數(shù)狡渌吮热绾笈_(tái)所以涉及到常量類型的數(shù)據(jù)要進(jìn)行大小端轉(zhuǎn)換裳仆,而且還要注意 short 類型轉(zhuǎn)換和 long類型轉(zhuǎn)換的方法不同
其他也沒(méi)有什么了