ios直播:自定義相機(jī)采集(GPUImage)湿滓、美顏滴须、硬/軟編碼(ffmpreg+x264)、流協(xié)議(rtmp和hls)叽奥,做一個(gè)直播項(xiàng)目

目前自己工作三年了扔水,但是自己卻沒有做過直播項(xiàng)目,一致對(duì)直播充滿了無限的好奇朝氓,所以自己在工作之余花了接近一個(gè)月的時(shí)間研究了一下魔市,與大家共勉,如有錯(cuò)誤或者不到位的地方赵哲,請(qǐng)指正待德。

直播的流程:

  • 視頻的采集 - 美顏與否 - 對(duì)視頻進(jìn)行編碼 - 服務(wù)器的流分發(fā)支持各個(gè)平臺(tái) - 客戶端進(jìn)行對(duì)流進(jìn)行解碼顯示播放。
    我用一個(gè)圖片來進(jìn)行表示枫夺。


    image.png

    解釋:下面我會(huì)對(duì)每一個(gè)部分進(jìn)行說明将宪,并且貼上我自己寫的代碼

  • 1.視頻的采集:
    視頻的采集對(duì)于我們iOS而言就是通過我們手機(jī)的前置和后置攝像頭采集到我們畫面還有通過麥克風(fēng)采集到聲音。
  • 1.1 我們通過系統(tǒng)的方法進(jìn)行采集,系統(tǒng)的方法進(jìn)行采集是沒有美顏效果的涧偷,首先蘋果很注重用戶的隱私也就是我們需要在我們的plist文件中添加使用相機(jī)的key 和使用麥克風(fēng)的key否則我們運(yùn)行程序會(huì)報(bào)錯(cuò)簸喂。怎么添加key 我用圖片的方式進(jìn)行表示:


    image.png

    添加key 我們還應(yīng)該調(diào)用代碼來判斷是否用戶允許我們使用相機(jī)等具體代碼是:

 switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
        case AVAuthorizationStatusAuthorized:
            {
                [self initVideoAndAudio];
            }
            break;
        case AVAuthorizationStatusNotDetermined:// 沒有選擇直接退出app 再次進(jìn)入直接提示
        {
            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
                if (granted) {
                    [self initVideoAndAudio];
                }else{
                    NSLog(@"這個(gè)用戶腦袋有病,沒有給出同意");
                }
            }];
        }
            break;
        case AVAuthorizationStatusRestricted:{ // 這種情況可能開始同意來 燎潮,后來自己又到設(shè)置里邊給關(guān)了
            
        }
        case AVAuthorizationStatusDenied:{ // 明確拒絕了
            
        }
        default:
            break;
    }

下面就是我們視頻喻鳄、音頻的采集。
先說幾個(gè)名詞确封,我用我網(wǎng)絡(luò)上和文檔中找的圖片來進(jìn)行顯示


image.png

其中一個(gè)AVCaptureSession來進(jìn)行管理輸入和輸出除呵,其中這個(gè)session 是一個(gè)單例 ,他也可以管理多個(gè)輸入和輸出爪喘。
其中session可以設(shè)置分辨率

if ([captureSession canSetSessionPreset:AVCaptureSessionPresetHigh]) {
    captureSession.sessionPreset = AVCaptureSessionPresetHigh;
}

分辨率對(duì)應(yīng)的表 是這樣的 也就是 設(shè)置分辨路屬性在不同的機(jī)器上是不一樣的颜曾,而且前后攝像頭設(shè)置同樣的屬性有時(shí)候也不一樣,因?yàn)榍昂笥布袝r(shí)候硬件不一樣秉剑。


image.png

對(duì)于session還需要設(shè)置我們的預(yù)覽涂層颂郎,沒有預(yù)覽涂層我們只能聽見聲音不能看到畫面艰管,預(yù)覽涂層可以設(shè)置frame 用來顯示我們能看到畫面的大小,值得說明的是有時(shí)候我們?cè)O(shè)置frame為全屏,但是顯示的畫面可能不是全屏盒犹,那是因?yàn)榉直媛实脑蛭覀冎恍枰逊直媛试O(shè)置高點(diǎn)就好了 (具體為什么我也不知道)骡澈。
session 有一個(gè)delegate 那里面有一個(gè)sampleBuffer 徽诲,其中sampleBuffer就是每一幀的視頻或者音頻赤赊,大家都知道視頻其實(shí)就是有一張張圖片組成,蘋果會(huì)給我們返回每一幀的數(shù)據(jù)渊涝,到時(shí)候我們就用每一幀的數(shù)據(jù)進(jìn)行編碼慎璧。
將我們的視頻的輸入輸出和音頻的輸出輸出添加到我們到session中,然后開始進(jìn)行采集跨释,具體代碼如下:

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>


@interface ViewController ()<AVCaptureVideoDataOutputSampleBufferDelegate,AVCaptureAudioDataOutputSampleBufferDelegate,AVCaptureFileOutputRecordingDelegate>

/**
 管理者
 */
@property (strong, nonatomic) AVCaptureSession *session;
/**
 音頻的輸入胸私、輸出
 */
@property (strong, nonatomic) AVCaptureDeviceInput *videoInput;
/**
 視頻輸出
 */
@property (strong, nonatomic) AVCaptureVideoDataOutput *videoOutput;
/**
 視頻的文件的寫入
 */
@property (strong, nonatomic) AVCaptureMovieFileOutput *movieFileOut;
/**
 預(yù)覽涂層
 */
@property (strong, nonatomic) AVCaptureVideoPreviewLayer *previewLayer;



@end

@implementation ViewController
-(AVCaptureSession *)session {
    if (!_session) {
        _session = [[AVCaptureSession alloc] init];
    }
    return _session;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    
//    AVAuthorizationStatusNotDetermined = 0,
//    AVAuthorizationStatusRestricted    = 1,
//    AVAuthorizationStatusDenied        = 2,
//    AVAuthorizationStatusAuthorized    = 3,
    // 先獲取權(quán)限
    switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
        case AVAuthorizationStatusAuthorized:
            {
                [self initVideoAndAudio];
            }
            break;
        case AVAuthorizationStatusNotDetermined:// 沒有選擇直接退出app 再次進(jìn)入直接提示
        {
            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
                if (granted) {
                    [self initVideoAndAudio];
                }else{
                    NSLog(@"這個(gè)用戶腦袋有病,沒有給出同意");
                }
            }];
        }
            break;
        case AVAuthorizationStatusRestricted:{ // 這種情況可能開始同意來 煤傍,后來自己又到設(shè)置里邊給關(guān)了
            
        }
        case AVAuthorizationStatusDenied:{ // 明確拒絕了
            
        }
        default:
            break;
    }
}

/**
 初始化音視頻的輸出 等等的操作
 */
- (void)initVideoAndAudio{
    // 設(shè)置分辨率
    if ([self.session canSetSessionPreset:AVCaptureSessionPresetHigh]) {
        [self.session setSessionPreset:AVCaptureSessionPresetHigh];
    }
    [self.session beginConfiguration];
    
    // 初始化視頻的輸入和輸出
    [self setUpVideoInputOutput];
    // 初始化音頻的輸入和輸出
    [self setUpAudioInputOutput];
    // 設(shè)置previewlayer
    [self setUpPreviewLayer];
    
    [self.session commitConfiguration];
}
/**
 初始化視頻的輸入和輸出
 */
- (void)setUpVideoInputOutput {
    
    // 設(shè)備
    AVCaptureDevice *videoDevice = [self captureDevice:AVMediaTypeVideo preferringPosition:AVCaptureDevicePositionFront];
    AVCaptureDeviceInput *videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:videoDevice error:nil];
    // 輸出
    AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init];
    [videoOutput setSampleBufferDelegate:self queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
    // 添加輸入輸出
    [self addInput:videoInput addOutput:videoOutput];
    self.videoInput = videoInput;
    self.videoOutput = videoOutput;

}
/**
 初始化音頻的輸入輸出
 */
- (void)setUpAudioInputOutput{
    // 設(shè)備
    AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
    // 輸入
    AVCaptureDeviceInput *audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:nil];
    // 輸出
    AVCaptureAudioDataOutput *audioOutput = [[AVCaptureAudioDataOutput alloc] init];
    [audioOutput setSampleBufferDelegate:self queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
    // 添加輸入輸出
    [self addInput:audioInput addOutput:audioOutput];
}

/**
 創(chuàng)建預(yù)覽涂層
 */
- (void)setUpPreviewLayer{
    // 創(chuàng)建預(yù)覽涂層
    AVCaptureVideoPreviewLayer *previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session];
    previewLayer.frame = self.view.bounds;
    [self.view.layer insertSublayer:previewLayer atIndex:0];
    self.previewLayer = previewLayer;
    
}

/**
 開始寫入視頻到本地
 */
- (void)startRecordVideo{
    
    // 設(shè)置輸出
    [self.session removeOutput:self.movieFileOut];
    AVCaptureMovieFileOutput *movieFileOut = [[AVCaptureMovieFileOutput alloc] init];
    self.movieFileOut = movieFileOut;
    // 設(shè)置connnection
    AVCaptureConnection *connection = [self.movieFileOut connectionWithMediaType:AVMediaTypeVideo];
    connection.automaticallyAdjustsVideoMirroring = YES;
    
    
    if ([self.session canAddOutput:self.movieFileOut]) {
        [self.session addOutput:self.movieFileOut];
        // 視頻穩(wěn)定設(shè)置
        if ([connection isVideoStabilizationSupported]) {
            connection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
        }
//        if ([connection isVideoMirroringSupported]) {
//            connection.videoMirrored = YES;
//        }
        connection.videoScaleAndCropFactor = connection.videoMaxScaleAndCropFactor;
    }
    // 開始錄制設(shè)置delegate
    NSString *pathStr = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"123.mp4"];
    NSURL *pathUrl = [NSURL fileURLWithPath:pathStr];
    [self.movieFileOut startRecordingToOutputFileURL:pathUrl recordingDelegate:self];
    
}
/**
 添加輸入和輸出

 @param input input
 @param output output
 */
- (void)addInput:(AVCaptureDeviceInput *)input addOutput:(AVCaptureOutput *)output{
    if([self.session canAddInput:input]){
        [self.session addInput:input];
    }
    if ([self.session canAddOutput:output]) {
        [self.session addOutput:output];
    }
}
/**
 創(chuàng)建capturedevice

 @param mediaType 類型
 @param position 位置
 @return 返回對(duì)象本身
 */
- (AVCaptureDevice *)captureDevice:(AVMediaType)mediaType preferringPosition:(AVCaptureDevicePosition)position{
    NSArray *devices = [AVCaptureDevice devicesWithMediaType:mediaType];
    AVCaptureDevice *captureDevice = devices.firstObject;
    for (AVCaptureDevice *device in devices) {
        if (device.position == position) {
            captureDevice = device;
        }
    }
    return captureDevice;
}
#pragma mark - 一些其他方法的響應(yīng) 主要是點(diǎn)擊事件的響應(yīng)
/**
 開始采集
 */
- (IBAction)startCollection {
    // 開始采集
    [self.session startRunning];
    // 開始錄制
  //  [self startRecordVideo];
}
/**
 停止采集
 */
- (IBAction)stopCollection {
    
    [self.session stopRunning];
}
/**
 保存到沙河
 */
- (IBAction)rotateDevice {
    // 先拿到之前的旋轉(zhuǎn)攝像頭
    if (!self.videoInput) {
        return;
    }
    AVCaptureDeviceInput *obtainInput;
    if (self.videoInput.device.position == AVCaptureDevicePositionFront) {
      AVCaptureDevice *device = [self captureDevice:AVMediaTypeVideo preferringPosition:AVCaptureDevicePositionBack];
      obtainInput = [[AVCaptureDeviceInput alloc] initWithDevice:device error:nil];
    }else{
      AVCaptureDevice *device = [self captureDevice:AVMediaTypeVideo preferringPosition:AVCaptureDevicePositionFront];
      obtainInput = [[AVCaptureDeviceInput alloc] initWithDevice:device error:nil];
    }
    [self.session removeInput:self.videoInput];
    if ([self.session canAddInput:obtainInput]) {
        [self.session addInput:obtainInput];
    }
    self.videoInput = obtainInput;

}
#pragma mark - delegate 的相關(guān)的事件

/**
 視頻和音頻的采集都經(jīng)過這個(gè)方法

 @param output 輸出
 @param sampleBuffer 每一幀
 @param connection 鏈接
 */
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{
  
    AVCaptureConnection *obtainConnection = [self.videoOutput connectionWithMediaType:AVMediaTypeVideo];
    if (obtainConnection == connection) {
        NSLog(@"開始采集視頻了");
    }else{
        NSLog(@"音頻");
    }
}
#pragma mark 錄制的想干的delegate
- (void)captureOutput:(AVCaptureFileOutput *)output didStartRecordingToOutputFileAtURL:(NSURL *)fileURL fromConnections:(NSArray<AVCaptureConnection *> *)connections{
    NSLog(@"開始錄制");
}
- (void)captureOutput:(AVCaptureFileOutput *)output didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray<AVCaptureConnection *> *)connections error:(nullable NSError *)error{
    NSLog(@"暫停錄制");
}

美顏相機(jī)就是我們對(duì)主播的磨皮盖文、高亮、曝光等等的處理蚯姆,一句話使丑的變帥變美。但是我們系統(tǒng)的方法目前是實(shí)現(xiàn)不了的洒敏,所以我使用的是GPUImage這個(gè)框架龄恋。下面我主要介紹的是美顏相機(jī)的做法和對(duì)圖片的一些處理

  • 美顏相機(jī)
    觀看GPUImage的官方文檔以及網(wǎng)上的一些資料可以看到做美顏相機(jī)大致的思路就是:設(shè)置預(yù)覽涂層、將預(yù)覽涂層添加到濾鏡上凶伙、將濾鏡添加到我們的源上然后進(jìn)行展示郭毕,當(dāng)然蘋果的plist文件添加隱私的key就不說了,因?yàn)樯弦黄恼掠姓f到函荣。我下面的這個(gè)例子显押,通過一個(gè)濾鏡組來進(jìn)行顯示扳肛,濾鏡組中包含了美白、 飽和乘碑、曝光挖息、磨皮 主播可以自己調(diào)試,讓自己顯得更美兽肤。下面是我的代碼:
#import "ViewController.h"
#import <GPUImage/GPUImage.h>
#import <AVFoundation/AVFoundation.h>
#import <MediaPlayer/MediaPlayer.h>

@interface ViewController ()<GPUImageVideoCameraDelegate>

// 創(chuàng)建攝像頭
@property (strong, nonatomic) GPUImageVideoCamera *camera;
@property (strong, nonatomic) GPUImageView *previewLayer;
// 創(chuàng)建幾個(gè)濾鏡
/**
 摩皮
 */
@property (strong, nonatomic) GPUImageBilateralFilter *bilaterFilter;
/**
 曝光
 */
@property (strong, nonatomic) GPUImageExposureFilter *exposureFilter;
/**
 美白
 */
@property (strong, nonatomic) GPUImageBrightnessFilter *brigtnessFilter;
/**
 飽和
 */
@property (strong, nonatomic) GPUImageSaturationFilter *saturationFilter;
/**
 創(chuàng)建寫入的文件
 */
@property (strong, nonatomic) GPUImageMovieWriter *movieWriter;

// 底部的view
@property (weak, nonatomic) IBOutlet UIView *bottomView;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomViewBottomConstaton;
@property (strong, nonatomic) MPMoviePlayerController *moviePlayer;
@property (copy, nonatomic) NSString *moviePath;
@end
@implementation ViewController
-(GPUImageVideoCamera *)camera {
    if (!_camera) {
        _camera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPresetHigh cameraPosition:AVCaptureDevicePositionFront];
    }
    return _camera;
}
-(GPUImageView *)previewLayer {
    if (!_previewLayer) {
        _previewLayer = [[GPUImageView alloc] initWithFrame:self.view.bounds];
    }
    return _previewLayer;
}
-(GPUImageMovieWriter *)movieWriter {
    if (!_movieWriter) {
        _movieWriter = [[GPUImageMovieWriter alloc] initWithMovieURL:[self obtainUrl] size:[UIScreen mainScreen].bounds.size];
    }
    return _movieWriter;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //初始化一些濾鏡
    self.bilaterFilter = [[GPUImageBilateralFilter alloc] init];
    self.exposureFilter = [[GPUImageExposureFilter alloc] init];
    self.brigtnessFilter = [[GPUImageBrightnessFilter alloc] init];
    self.saturationFilter = [[GPUImageSaturationFilter alloc] init];
    // 調(diào)整攝像頭的方向
    self.camera.outputImageOrientation = UIInterfaceOrientationPortrait;
    // 調(diào)整攝像頭的鏡像 自己動(dòng)的方向和鏡子中的方向一致
    self.camera.horizontallyMirrorFrontFacingCamera = YES;
    // 創(chuàng)建過濾層
    GPUImageFilterGroup *filterGroup = [self obtainFilterGroup];
    [self.camera addTarget:filterGroup];
    // 將imageview 添加到過濾層上
    [filterGroup addTarget:self.previewLayer];
    [self.view insertSubview:self.previewLayer atIndex:0];
    // 開始拍攝
    [self.camera startCameraCapture];
#pragma mark - 開始寫入視頻
    self.movieWriter.encodingLiveVideo = YES;
    [filterGroup addTarget:self.movieWriter];
    self.camera.delegate = self;
    self.camera.audioEncodingTarget = self.movieWriter;
    // 開始錄制
    [self.movieWriter startRecording];
}
/**
 獲取緩存的路徑

 @return 獲取到自己想要的url
 */
- (NSURL *)obtainUrl{
  
    NSString *pathStr = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"456.mp4"];
    self.moviePath = pathStr;
    // 判斷路徑是否存在
    if ([[NSFileManager defaultManager] fileExistsAtPath:pathStr]) {
        [[NSFileManager defaultManager] removeItemAtPath:pathStr error:nil];
    }
    NSURL *url = [NSURL fileURLWithPath:pathStr];
    return url;
}
/**
 創(chuàng)建過濾組
 */
- (GPUImageFilterGroup *)obtainFilterGroup{
    
    GPUImageFilterGroup *group = [[GPUImageFilterGroup alloc] init];
    // 按照順序組成一個(gè)鏈
    [self.bilaterFilter addTarget:self.exposureFilter];
    [self.exposureFilter addTarget:self.brigtnessFilter];
    [self.brigtnessFilter addTarget:self.saturationFilter];
    // 將濾鏡添加到濾鏡組中(開始和結(jié)尾)
    group.initialFilters = @[self.bilaterFilter];
    group.terminalFilter = self.saturationFilter;
    
    return group;
}
#pragma mark - 相關(guān)按鈕的點(diǎn)擊事件

/**
 結(jié)束直播相關(guān)的事件

 @param sender 按鈕
 */
- (IBAction)endLiveAction:(UIButton *)sender {
   
    [self.camera stopCameraCapture];
    [self.previewLayer removeFromSuperview];
    [self.movieWriter finishRecording];
}
/**
 開始播放視頻

 @param sender 按鈕
 */
- (IBAction)startPlayAction:(UIButton *)sender {
    
    MPMoviePlayerController *moviePlayer = [[MPMoviePlayerController alloc] initWithContentURL:[NSURL fileURLWithPath:self.moviePath]];
    moviePlayer.view.frame = self.view.bounds;
    moviePlayer.fullscreen = YES;
    [self.view addSubview:moviePlayer.view];
    [moviePlayer play];
    self.moviePlayer = moviePlayer;
    
}

/**
 點(diǎn)擊彈出需要設(shè)備的美顏參數(shù)

 @param sender 按鈕
 */
- (IBAction)beautufulViewAction:(UIButton *)sender {
    if (self.bottomViewBottomConstaton.constant == -250) {
        self.bottomViewBottomConstaton.constant = 0;
    }else{
        self.bottomViewBottomConstaton.constant = -250;
    }
    [UIView animateWithDuration:0.25 animations:^{
        [self.view layoutIfNeeded];
    }];
}

/**
 切換前后攝像頭

 @param sender 按鈕
 */
- (IBAction)switchFontAndBehindCameraAction:(UIButton *)sender {
    
    [self.camera rotateCamera];
    
}

/**
 開啟或者關(guān)閉美顏

 @param sender 按鈕
 */
- (IBAction)closeOrOpenBeautifulAction:(UISwitch *)sender {
    if (sender.isOn) {
        [self.camera removeAllTargets];
        GPUImageFilterGroup *group = [self obtainFilterGroup];
        [self.camera addTarget:group];
        [group addTarget:self.previewLayer];
        
    }else{
        [self.camera removeAllTargets];
        [self.camera addTarget:self.previewLayer];
    }
}
/**
 磨皮的slider的事件

 @param sender 按鈕
 */
- (IBAction)mopiSliderAction:(UISlider *)sender {
    
    self.bilaterFilter.distanceNormalizationFactor = sender.value * 0.3;
    
}
/**
 曝光的按鈕的點(diǎn)擊事件

 @param sender 按鈕
 */
- (IBAction)baoguangSliderAction:(UISlider *)sender {
    
    self.exposureFilter.exposure = sender.value;
    
}

/**
 美白的按鈕的點(diǎn)擊事件

 @param sender 按鈕
 */
- (IBAction)meibaiSliderAction:(UISlider *)sender {
    
    self.brigtnessFilter.brightness = sender.value;
}

/**
 飽和的按鈕的點(diǎn)擊事件

 @param sender 按鈕
 */
- (IBAction)baoheSliderAction:(UISlider *)sender {
    
    self.saturationFilter.saturation = sender.value;
    
}
#pragma mark - camera 的 delegate

- (void)willOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer{
    NSLog(@"+++++++++++++++++");
}
@end

有沒有發(fā)現(xiàn) 使用GPUIImage做的相機(jī) 是不是很簡(jiǎn)單套腹,我曾經(jīng)大致看一下他的原碼,里面他也是封裝系統(tǒng)的方法 只不過他加了自己的一些矩陣 等等的算法和一些相關(guān)的圖形處理的相關(guān)的知識(shí)(這個(gè)人雖然看他的寫的代碼風(fēng)格一般但是他的專功知識(shí)還是挺六的资铡,看了他的git 這個(gè)10000多個(gè)星电禀,其他的他的框架星星很少)

  • 下面我做的圖片增加濾鏡的 ,實(shí)現(xiàn)思路和上邊一樣笤休,代碼寫的一般只是為了實(shí)現(xiàn)功能尖飞,并沒有做過多的抽取以及封裝。
#import "ViewController.h"
#import "GPUImage-umbrella.h"

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UIImageView *displayImageView;


@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
   
    
}

/**
 褐色的按鈕的點(diǎn)擊事件

 @param sender 阿牛
 */
- (IBAction)brownAction:(UIButton *)sender {
    
    self.displayImageView.image = [UIImage imageNamed:@"test"];
    GPUImageSepiaFilter *brownFilter = [[GPUImageSepiaFilter alloc] init];
    // 設(shè)置渲染區(qū)域
    [brownFilter forceProcessingAtSize:self.displayImageView.image.size];
    [brownFilter useNextFrameForImageCapture];
    // 創(chuàng)建數(shù)據(jù)源
    GPUImagePicture *imageSource = [[GPUImagePicture alloc] initWithImage:self.displayImageView.image];
    // 將濾鏡添加到數(shù)據(jù)源上
    [imageSource addTarget:brownFilter];
    // 開始渲染
    [imageSource processImage];
    // 生成新的圖片
    UIImage *image = [brownFilter imageFromCurrentFramebuffer];
    self.displayImageView.image = image;
    
    
//    self.displayImageView.image = [UIImage imageNamed:@"test"];
//    // 添加黑白素描濾鏡
//    GPUImageSketchFilter *stillFilter = [[GPUImageSketchFilter alloc] init];
//    // 設(shè)置渲染區(qū)域
//    [stillFilter forceProcessingAtSize:self.displayImageView.image.size];
//    [stillFilter useNextFrameForImageCapture];
//    // 獲取數(shù)據(jù)源
//    GPUImagePicture *stilImageSource = [[GPUImagePicture alloc] initWithImage:self.displayImageView.image];
//    // 添加濾鏡
//    [stilImageSource addTarget:stillFilter];
//    // 開始渲染
//    [stilImageSource processImage];
//    // 生成新的圖片
//    UIImage *newImage = [stillFilter imageFromCurrentFramebuffer];
//    self.displayImageView.image = newImage;
    
}
/**
 卡通的事件

 @param sender 按鈕
 */
- (IBAction)cartoon:(UIButton *)sender {
    
    self.displayImageView.image = [UIImage imageNamed:@"test"];
    GPUImageToonFilter *cartoolFilter = [[GPUImageToonFilter alloc] init];
    // 創(chuàng)建數(shù)據(jù)源
    GPUImagePicture *imageSource = [[GPUImagePicture alloc] initWithImage:self.displayImageView.image];
    // 設(shè)置渲染區(qū)域
    [cartoolFilter forceProcessingAtSize:self.displayImageView.image.size];
    [cartoolFilter useNextFrameForImageCapture];
    // 將濾鏡添加到數(shù)據(jù)源上
    [imageSource addTarget:cartoolFilter];
    // 開始渲染
    [imageSource processImage];
    // 生成新的圖片
    UIImage *image = [cartoolFilter imageFromCurrentFramebuffer];
    self.displayImageView.image = image;

    
}
/**
 素描的事件

 @param sender 按鈕
 */
- (IBAction)sketch:(UIButton *)sender {
    
    self.displayImageView.image = [UIImage imageNamed:@"test"];
    GPUImageSketchFilter *sketchFileter = [[GPUImageSketchFilter alloc] init];
    [self filterImage:sketchFileter];
    
    
}

/**
 浮雕的事件

 @param sender 按鈕
 */
- (IBAction)reliefAction:(UIButton *)sender {
    
    self.displayImageView.image = [UIImage imageNamed:@"test"];
    GPUImageEmbossFilter *embossFilter = [[GPUImageEmbossFilter alloc] init];
    // 設(shè)置渲染區(qū)域
    [embossFilter forceProcessingAtSize:self.displayImageView.image.size];
    [embossFilter useNextFrameForImageCapture];
    // 創(chuàng)建數(shù)據(jù)源
    GPUImagePicture *imageSource = [[GPUImagePicture alloc] initWithImage:self.displayImageView.image];
    // 將濾鏡添加到數(shù)據(jù)源上
    [imageSource addTarget:embossFilter];
    // 開始渲染
    [imageSource processImage];
    // 生成新的圖片
    UIImage *image = [embossFilter imageFromCurrentFramebuffer];
    self.displayImageView.image = image;
}

- (UIImage *)filterImage:(GPUImageSobelEdgeDetectionFilter *)filter {
    
    // 創(chuàng)建數(shù)據(jù)源
    GPUImagePicture *imageSource = [[GPUImagePicture alloc] initWithImage:self.displayImageView.image];
    // 設(shè)置渲染區(qū)域
    [filter forceProcessingAtSize:self.displayImageView.image.size];
    [filter useNextFrameForImageCapture];
    // 將濾鏡添加到數(shù)據(jù)源上
    [imageSource addTarget:filter];
    // 開始渲染
    [imageSource processImage];
    // 生成新的圖片
    UIImage *image = [filter imageFromCurrentFramebuffer];
    return image;
}
/**
 創(chuàng)建一個(gè)黑白的素描的圖片
 */
- (void)createSketchImage {
   
    
    self.displayImageView.image = [UIImage imageNamed:@"test"];
    // 添加黑白素描濾鏡
    GPUImageSketchFilter *stillFilter = [[GPUImageSketchFilter alloc] init];
    // 設(shè)置渲染區(qū)域
    [stillFilter forceProcessingAtSize:self.displayImageView.image.size];
    [stillFilter useNextFrameForImageCapture];
    // 獲取數(shù)據(jù)源
    GPUImagePicture *stilImageSource = [[GPUImagePicture alloc] initWithImage:self.displayImageView.image];
    // 添加濾鏡
    [stilImageSource addTarget:stillFilter];
    // 開始渲染
    [stilImageSource processImage];
    // 生成新的圖片
    UIImage *newImage = [stillFilter imageFromCurrentFramebuffer];
    self.displayImageView.image = newImage;
}

@end

正常的圖片:


image.png

加了褐色濾鏡的:


image.png

加了卡通濾鏡的:
image.png

素描的濾鏡:


image.png

浮雕的濾鏡:
image.png

基本的簡(jiǎn)單使用就是這些 店雅,但是要想好好的掌握GPUImage 這些遠(yuǎn)遠(yuǎn)不夠政基,可以去官方文檔上去看說的也挺詳細(xì)的 ,還有大家網(wǎng)上一搜 很多的各種濾鏡的解釋都有 我們主要的目的是直播的原理 以及流程這里不在做多的說明底洗。
  • 我們錄制的視頻經(jīng)過美顏之后我們就應(yīng)該對(duì)視頻進(jìn)行編碼腋么,編碼方式其中分為硬編碼和軟編碼,其中蘋果在ios8之后出了一套框架亥揖,我們利用這個(gè)框架可以進(jìn)行硬編碼珊擂,不過基本上是純c的
    首先我們需要明白做直播的視頻為什么要經(jīng)過編碼:因?yàn)檎5臎]有經(jīng)過編碼的視頻是非常非常大的,我們本來做直播需要把視頻傳遞給服務(wù)器 然后讓服務(wù)器進(jìn)行流分發(fā)费变,如果過大 即使在wifi的情況下 還是會(huì)卡頓 或者造成視頻的顯示問題摧扇。
    其次我們需要研究視頻構(gòu)成或者說視頻有哪些是我們所謂說的冗余
  • 其中視頻的冗余有:時(shí)間、空間挚歧、還有我們的人眼冗余
    1.時(shí)間上的:比如多張圖片基本一樣 只有一個(gè)細(xì)小的差別扛稽,這種我們只需要記錄一張共同的圖片和其他的不同點(diǎn)就可以了 ,比如這個(gè)就是時(shí)間上的冗余:


    image.png
  1. 空間上的冗余:
    空間上的冗余是很多的點(diǎn)都一樣的滑负,比圖我這張圖片:


    image.png
  2. 還有就是我們的視覺冗余:
    人體的視覺對(duì)高頻信息不敏感在张,對(duì)運(yùn)動(dòng)、高對(duì)比度信息等等更敏感矮慕,所以我們編碼視頻可以去掉高頻的信息帮匾。
    如:


    image.png
  • 下面我們可以對(duì)視頻進(jìn)行壓縮編碼,但是壓縮編碼我們需要按照一定的標(biāo)準(zhǔn)否則到時(shí)候進(jìn)行解碼解碼不了痴鳄,現(xiàn)在我說一下一些編碼標(biāo)準(zhǔn):
  1. h.26X系列的
    1.1 h.261 主要在老的視頻會(huì)議和老的視頻電話中使用瘟斜。
    1.2 h.263 主要用電視頻會(huì)議和視頻電話中和一些網(wǎng)絡(luò)視頻上。
    1.3 h.264 是一種高精度的壓縮標(biāo)準(zhǔn)、廣泛被使用在視頻的錄制螺句、壓縮和發(fā)布格式
    1.4 h.265 是一種更高效率的壓縮標(biāo)準(zhǔn)虽惭、可支持4K分辨率甚至到超高畫質(zhì)電視,最高分辨率可達(dá)到8192×4320(8K分辨率)蛇尚,但是現(xiàn)在市場(chǎng)上沒有被大眾普遍使用芽唇,而且穩(wěn)定性不確定,所以目前最常用的是h.264
  2. MPEG系列(由ISO[國(guó)際標(biāo)準(zhǔn)組織機(jī)構(gòu)]下屬的MPEG[運(yùn)動(dòng)圖象專家組]開發(fā))
    2.1MPEG-1第二部分:MPEG-1第二部分主要使用在VCD上佣蓉,有些在線視頻也使用這種格式
    2.2 MPEG-2第二部分(MPEG-2第二部分等同于H.262披摄,使用在DVD、SVCD和大多數(shù)數(shù)字視頻廣播系統(tǒng)中.
  • h.264的編碼大致流程:
    個(gè)人理解 首先他會(huì)生成一個(gè)圖像幀a勇凭,然后他會(huì)生成圖像幀b 疚膊,但是生成b的時(shí)候他只會(huì)生成與生成與a 有差別的地方,同理圖像幀c也是這個(gè)道理虾标,以此類推寓盗,這樣的一組圖像幀我們稱之為一個(gè)序列,序列就是有相同特點(diǎn)的圖像幀璧函,當(dāng)發(fā)現(xiàn)圖像幀和a的差別很大 那就從新生成一個(gè)序列傀蚌。
  1. h .264定義了三種幀:
    i幀:生成的完整的幀叫i幀
    p幀:參考之前a幀生成的只包含差異的幀叫p幀
    b幀:由i幀和p幀生成的幀叫b幀
  2. h.264參考的核心算法是幀間壓縮和幀內(nèi)壓縮
    幀內(nèi)壓縮:主要對(duì)i幀
    幀間壓縮:主要是p幀和b幀
  3. h.264分層設(shè)計(jì):
    3.1
    視頻編碼層:
    負(fù)責(zé)高效率的內(nèi)容表示。
    網(wǎng)絡(luò)提取層:
    負(fù)責(zé)網(wǎng)絡(luò)恰當(dāng)?shù)姆绞竭M(jìn)行打包和傳送蘸吓。這樣視頻封裝和網(wǎng)絡(luò)友好行分別由vcl和nal分別完成善炫。我們之前學(xué)習(xí)的編碼方式都是vcl。
    nal設(shè)計(jì)的目的:
    根據(jù)不同的網(wǎng)絡(luò)環(huán)境库继,將vcl產(chǎn)生的比特字符串友好的適配到各個(gè)網(wǎng)絡(luò)環(huán)境中箩艺。
    nal的組成:
    有一個(gè)nal序列單元組成,分別為nal的頭和nal的體組成宪萄。
    nal的頭通常由00 00 00 01擔(dān)任艺谆,并且作為一個(gè)新的nal的開始
    nal的體中瘋轉(zhuǎn)vcl的編碼信息或者其他的信息。
    封裝過程:
    i幀p幀或者b幀 都被封裝成一個(gè)nal單元或者n個(gè)nal單元進(jìn)行存儲(chǔ)拜英。
    i幀開始之前也有非vcl的nal單元静汤,用于保存其他的信息 比如:pps (圖像參數(shù)集)、sps(序列參數(shù)集)居凶。
    一般的流程都是先編輯pps虫给、sps 然后是i幀 、p幀侠碧、b幀狰右。
  • 編碼方式:
    硬編碼:使用非cpu進(jìn)行編碼,如顯卡gpu 舆床、專用的dsp等。
    軟編碼:使用cpu進(jìn)行編碼,一般都是ffmpeg+x264.
    兩種方式對(duì)比:
    軟編碼修改參數(shù)簡(jiǎn)單挨队,實(shí)現(xiàn)簡(jiǎn)單谷暮,升級(jí)易但是耗費(fèi)cpu的資源大,手機(jī)容易發(fā)燙盛垦。
    硬編碼消耗cpu資源少湿弦,但是參數(shù)調(diào)整不如軟編碼方便,代碼基本固定腾夯。
  • 了解了編碼方式以及編碼的標(biāo)準(zhǔn)颊埃,下面是我的硬編碼的實(shí)現(xiàn):
VideoEncoder.h 文件:
#import <UIKit/UIKit.h>
#import <VideoToolbox/VideoToolbox.h>

@interface VideoEncoder : NSObject

- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer;
- (void)endEncode;

@end
VideoEncoder.m文件:
#import "VideoEncoder.h"

@interface VideoEncoder()

/** 記錄當(dāng)前的幀數(shù) */
@property (nonatomic, assign) NSInteger frameID;

/** 編碼會(huì)話 */
@property (nonatomic, assign) VTCompressionSessionRef compressionSession;

/** 文件寫入對(duì)象 */
@property (nonatomic, strong) NSFileHandle *fileHandle;

@end

@implementation VideoEncoder

- (instancetype)init {
    if (self = [super init]) {
        // 1.初始化寫入文件的對(duì)象(NSFileHandle用于寫入二進(jìn)制文件)
        [self setupFileHandle];
        
        // 2.初始化壓縮編碼的會(huì)話
        [self setupVideoSession];
    }
    
    return self;
}

- (void)setupFileHandle {
    // 1.獲取沙盒路徑
    NSString *file = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"abc.h264"];
    
    // 2.如果原來有文件,則刪除
    [[NSFileManager defaultManager] removeItemAtPath:file error:nil];
    [[NSFileManager defaultManager] createFileAtPath:file contents:nil attributes:nil];
    
    // 3.創(chuàng)建對(duì)象
    self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:file];
}


- (void)setupVideoSession {
    // 1.用于記錄當(dāng)前是第幾幀數(shù)據(jù)(畫面幀數(shù)非常多)
    self.frameID = 0;
    
    // 2.錄制視頻的寬度&高度
    int width = [UIScreen mainScreen].bounds.size.width;
    int height = [UIScreen mainScreen].bounds.size.height;
    
    // 3.創(chuàng)建CompressionSession對(duì)象,該對(duì)象用于對(duì)畫面進(jìn)行編碼
    // kCMVideoCodecType_H264 : 表示使用h.264進(jìn)行編碼
    // didCompressH264 : 當(dāng)一次編碼結(jié)束會(huì)在該函數(shù)進(jìn)行回調(diào),可以在該函數(shù)中將數(shù)據(jù),寫入文件中
    VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)(self),  &_compressionSession);
    
    // 4.設(shè)置實(shí)時(shí)編碼輸出(直播必然是實(shí)時(shí)輸出,否則會(huì)有延遲)
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
    
    // 5.設(shè)置期望幀率(每秒多少幀,如果幀率過低,會(huì)造成畫面卡頓)
    int fps = 30;
    CFNumberRef  fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
    
    
    // 6.設(shè)置碼率(碼率: 編碼效率, 碼率越高,則畫面越清晰, 如果碼率較低會(huì)引起馬賽克 --> 碼率高有利于還原原始畫面,但是也不利于傳輸)
    int bitRate = 800*1024;
    CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);
    // 這是一個(gè)算法
    NSArray *limit = @[@(bitRate * 1.5/8), @(1)];
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);
    
    // 7.設(shè)置關(guān)鍵幀(GOPsize)間隔
    int frameInterval = 30;
    CFNumberRef  frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);
    
    // 8.基本設(shè)置結(jié)束, 準(zhǔn)備進(jìn)行編碼
    VTCompressionSessionPrepareToEncodeFrames(self.compressionSession);
}


// 編碼完成回調(diào)
void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) {
    
    // 1.判斷狀態(tài)是否等于沒有錯(cuò)誤
    if (status != noErr) {
        return;
    }
    
    // 2.根據(jù)傳入的參數(shù)獲取對(duì)象
    VideoEncoder* encoder = (__bridge VideoEncoder*)outputCallbackRefCon;
    
    // 3.判斷是否是關(guān)鍵幀
    bool isKeyframe = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
    // 判斷當(dāng)前幀是否為關(guān)鍵幀
    // 獲取sps & pps數(shù)據(jù)
    if (isKeyframe)
    {
        // 獲取編碼后的信息(存儲(chǔ)于CMFormatDescriptionRef中)
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
        
        // 獲取SPS信息
        size_t sparameterSetSize, sparameterSetCount;
        const uint8_t *sparameterSet;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0 );
        
        // 獲取PPS信息
        size_t pparameterSetSize, pparameterSetCount;
        const uint8_t *pparameterSet;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0 );
        
        // 裝sps/pps轉(zhuǎn)成NSData,以方便寫入文件
        NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
        NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
        
        // 寫入文件
        [encoder gotSpsPps:sps pps:pps];
    }
    
    // 獲取數(shù)據(jù)塊
    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    size_t length, totalLength;
    char *dataPointer;
    OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer);
    if (statusCodeRet == noErr) {
        size_t bufferOffset = 0;
        static const int AVCCHeaderLength = 4; // 返回的nalu數(shù)據(jù)前四個(gè)字節(jié)不是0001的startcode蝶俱,而是大端模式的幀長(zhǎng)度length
        
        // 循環(huán)獲取nalu數(shù)據(jù)
        while (bufferOffset < totalLength - AVCCHeaderLength) {
            uint32_t NALUnitLength = 0;
            // Read the NAL unit length
            memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
            
            // 從大端轉(zhuǎn)系統(tǒng)端
            NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
            
            NSData* data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];
            [encoder gotEncodedData:data isKeyFrame:isKeyframe];
            
            // 移動(dòng)到寫一個(gè)塊班利,轉(zhuǎn)成NALU單元
            // Move to the next NAL unit in the block buffer
            bufferOffset += AVCCHeaderLength + NALUnitLength;
        }
    }
}

- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps
{
    // 1.拼接NALU的header
    const char bytes[] = "\x00\x00\x00\x01";
    size_t length = (sizeof bytes) - 1;
    NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
    
    // 2.將NALU的頭&NALU的體寫入文件
    [self.fileHandle writeData:ByteHeader];
    [self.fileHandle writeData:sps];
    [self.fileHandle writeData:ByteHeader];
    [self.fileHandle writeData:pps];
    
}
- (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame
{
    NSLog(@"gotEncodedData %d", (int)[data length]);
    if (self.fileHandle != NULL)
    {
        const char bytes[] = "\x00\x00\x00\x01";
        size_t length = (sizeof bytes) - 1; //string literals have implicit trailing '\0'
        NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
        [self.fileHandle writeData:ByteHeader];
        [self.fileHandle writeData:data];
    }
}

- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer {
    // 1.將sampleBuffer轉(zhuǎn)成imageBuffer
    CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
    
    // 2.根據(jù)當(dāng)前的幀數(shù),創(chuàng)建CMTime的時(shí)間
    CMTime presentationTimeStamp = CMTimeMake(self.frameID++, 1000);
    VTEncodeInfoFlags flags;
    
    // 3.開始編碼該幀數(shù)據(jù)
    OSStatus statusCode = VTCompressionSessionEncodeFrame(self.compressionSession,
                                                          imageBuffer,
                                                          presentationTimeStamp,
                                                          kCMTimeInvalid,
                                                          NULL, (__bridge void * _Nullable)(self), &flags);
    if (statusCode == noErr) {
        NSLog(@"H264: VTCompressionSessionEncodeFrame Success");
    }
}

- (void)endEncode {
    VTCompressionSessionCompleteFrames(self.compressionSession, kCMTimeInvalid);
    VTCompressionSessionInvalidate(self.compressionSession);
    CFRelease(self.compressionSession);
    self.compressionSession = NULL;
}
@end
  • 軟編碼的實(shí)現(xiàn):
    軟編碼的環(huán)境搭建要復(fù)雜些:其中電腦中要先安裝ffmpeg
    順便說下ffmpeg他是純c的 他可以分流 也可以推流 是一些高手中的高手集成的命令行工具。感興趣的可以繼續(xù)研究一下榨呆,我對(duì)此理解的不是很深罗标。

Mac安裝/使用FFmpeg

  • 安裝
    ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
    brew install ffmpeg
  • 簡(jiǎn)單使用
    轉(zhuǎn)化格式: ffmpeg -i 你的視頻.webm 你的視頻.mp4
    分離視頻: ffmpeg -i 你的視頻.mp4 -vcodec copy -an 你的視頻.mp4
    分離音頻: ffmpeg -i 你的視頻.mp4 -acodec copy -vn 你的視頻.aac

編譯FFmpeg(iOS)

  • 下載編譯FFmpeg所需要的腳本文件gas-preprocessor.pl
    • 下載地址: https://github.com/mansr/gas-preprocessor
    • 復(fù)制gas-preprocessor.pl到/usr/sbin下,(這個(gè)應(yīng)該是復(fù)制到/usr/local/bin)
    • 修改文件權(quán)限:chmod 777 /usr/local/bin/gas-preprocessor.pl
  • 下載腳本FFmpeg腳本

編譯X264

  • x264官網(wǎng) 下載x264源碼闯割,將其文件夾名稱改為x264
  • https://www.videolan.org/developers/x264.html
  • 下載gas-preprocessor(FFmpeg編譯時(shí)已經(jīng)下載過)
  • 下載x264 build shell
  • 修改權(quán)限/執(zhí)行腳本
    • sudo chmod u+x build-x264.sh
    • sudo ./build-x264.sh
      最后得到的是這兩個(gè)文件夾


      image.png

      如果生成的文件夾里面包含這些東西 基本就對(duì)了宙拉。
      下面是我的軟編碼實(shí)現(xiàn):(實(shí)話說硬編碼我還能寫出一部分,軟編碼我真的一點(diǎn)寫不出來丙笋,但是我找的資料還可以 我基本弄明白了谢澈,根據(jù)我說的視頻壓縮標(biāo)準(zhǔn),應(yīng)該知道每一部分在干什么不见,基本代碼固定會(huì)調(diào)試參數(shù)就行了)

X264Manager.h文件:
#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>

@interface X264Manager : NSObject

/*
 * 設(shè)置編碼后文件的保存路徑
 */
- (void)setFileSavedPath:(NSString *)path;

/*
 * 設(shè)置X264
 * 0: 成功澳化; -1: 失敗
 * width: 視頻寬度
 * height: 視頻高度
 * bitrate: 視頻碼率,碼率直接影響編碼后視頻畫面的清晰度稳吮, 越大越清晰缎谷,但是為了便于保證編碼后的數(shù)據(jù)量不至于過大,以及適應(yīng)網(wǎng)絡(luò)帶寬傳輸灶似,就需要合適的選擇該值
 */
- (int)setX264ResourceWithVideoWidth:(int)width height:(int)height bitrate:(int)bitrate;

/*
 * 將CMSampleBufferRef格式的數(shù)據(jù)編碼成h264并寫入文件
 */
- (void)encoderToH264:(CMSampleBufferRef)sampleBuffer;

/*
 * 釋放資源
 */
- (void)freeX264Resource;
@end
X264Manager.m 文件:
#import "X264Manager.h"

#ifdef __cplusplus
extern "C" {
#endif
    
#include <libavutil/opt.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

#ifdef __cplusplus
};
#endif


/*
 編碼之后&解碼之前的畫面: AVFrame --> 內(nèi)容寫入文件
 編碼之前&解碼之后的畫面: AVPackage --> 解碼之后, 使用OpenGLES渲染
 */

@implementation X264Manager
{
    AVFormatContext                     *pFormatCtx;
    AVOutputFormat                      *fmt;
    AVStream                            *video_st;
    AVCodecContext                      *pCodecCtx;
    AVCodec                             *pCodec;
    AVPacket                             pkt;
    uint8_t                             *picture_buf;
    AVFrame                             *pFrame;
    int                                  picture_size;
    int                                  y_size;
    int                                  framecnt;
    char                                *out_file;
    
    int                                  encoder_h264_frame_width; // 編碼的圖像寬度
    int                                  encoder_h264_frame_height; // 編碼的圖像高度
}



/*
 * 設(shè)置編碼后文件的文件名列林,保存路徑
 */
- (void)setFileSavedPath:(NSString *)path;
{
    out_file = [self nsstring2char:path];
}

/*
 * 將路徑轉(zhuǎn)成C語言字符串(傳入路徑為C字符串)
 */
- (char*)nsstring2char:(NSString *)path
{

    NSUInteger len = [path length];
    char *filepath = (char*)malloc(sizeof(char) * (len + 1));
    
    [path getCString:filepath maxLength:len + 1 encoding:[NSString defaultCStringEncoding]];
    
    return filepath;
}


/*
 *  設(shè)置X264
 */
- (int)setX264ResourceWithVideoWidth:(int)width height:(int)height bitrate:(int)bitrate
{
    // 1.默認(rèn)從第0幀開始(記錄當(dāng)前的幀數(shù))
    framecnt = 0;
    
    // 2.記錄傳入的寬度&高度
    encoder_h264_frame_width = width;
    encoder_h264_frame_height = height;
    
    // 3.注冊(cè)FFmpeg所有編解碼器(無論編碼還是解碼都需要該步驟)
    av_register_all();
    
    // 4.初始化AVFormatContext: 用作之后寫入視頻幀并編碼成 h264,貫穿整個(gè)工程當(dāng)中(釋放資源時(shí)需要銷毀)
    pFormatCtx = avformat_alloc_context();
    
    // 5.設(shè)置輸出文件的路徑
    fmt = av_guess_format(NULL, out_file, NULL);
    pFormatCtx->oformat = fmt;
    
    // 6.打開文件的緩沖區(qū)輸入輸出酪惭,flags 標(biāo)識(shí)為  AVIO_FLAG_READ_WRITE 希痴,可讀寫
    if (avio_open(&pFormatCtx->pb, out_file, AVIO_FLAG_READ_WRITE) < 0){
        printf("Failed to open output file! \n");
        return -1;
    }
    
    // 7.創(chuàng)建新的輸出流, 用于寫入文件
    video_st = avformat_new_stream(pFormatCtx, 0);
    
    // 8.設(shè)置 20 幀每秒 ,也就是 fps 為 20
    video_st->time_base.num = 1;
    video_st->time_base.den = 25;
    
    if (video_st==NULL){
        return -1;
    }
    
    // 9.pCodecCtx 用戶存儲(chǔ)編碼所需的參數(shù)格式等等
    // 9.1.從媒體流中獲取到編碼結(jié)構(gòu)體春感,他們是一一對(duì)應(yīng)的關(guān)系砌创,一個(gè) AVStream 對(duì)應(yīng)一個(gè)  AVCodecContext
    pCodecCtx = video_st->codec;
    
    // 9.2.設(shè)置編碼器的編碼格式(是一個(gè)id)虏缸,每一個(gè)編碼器都對(duì)應(yīng)著自己的 id,例如 h264 的編碼 id 就是 AV_CODEC_ID_H264
    pCodecCtx->codec_id = fmt->video_codec;
    
    // 9.3.設(shè)置編碼類型為 視頻編碼
    pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
    
    // 9.4.設(shè)置像素格式為 yuv 格式
    pCodecCtx->pix_fmt = PIX_FMT_YUV420P;
    
    // 9.5.設(shè)置視頻的寬高
    pCodecCtx->width = encoder_h264_frame_width;
    pCodecCtx->height = encoder_h264_frame_height;
    
    // 9.6.設(shè)置幀率
    pCodecCtx->time_base.num = 1;
    pCodecCtx->time_base.den = 25;
    
    // 9.7.設(shè)置碼率(比特率)
    pCodecCtx->bit_rate = bitrate;
    
    // 9.8.視頻質(zhì)量度量標(biāo)準(zhǔn)(常見qmin=10, qmax=51)
    pCodecCtx->qmin = 10;
    pCodecCtx->qmax = 51;
    
    // 9.9.設(shè)置圖像組層的大小(GOP-->兩個(gè)I幀之間的間隔)
    pCodecCtx->gop_size = 30;
    
    // 9.10.設(shè)置 B 幀最大的數(shù)量嫩实,B幀為視頻圖片空間的前后預(yù)測(cè)幀刽辙, B 幀相對(duì)于 I、P 幀來說甲献,壓縮率比較大宰缤,也就是說相同碼率的情況下,
    // 越多 B 幀的視頻晃洒,越清晰慨灭,現(xiàn)在很多打視頻網(wǎng)站的高清視頻,就是采用多編碼 B 幀去提高清晰度球及,
    // 但同時(shí)對(duì)于編解碼的復(fù)雜度比較高氧骤,比較消耗性能與時(shí)間
    pCodecCtx->max_b_frames = 5;
    
    // 10.可選設(shè)置
    AVDictionary *param = 0;
    // H.264
    if(pCodecCtx->codec_id == AV_CODEC_ID_H264) {
        // 通過--preset的參數(shù)調(diào)節(jié)編碼速度和質(zhì)量的平衡。
        av_dict_set(&param, "preset", "slow", 0);
        
        // 通過--tune的參數(shù)值指定片子的類型桶略,是和視覺優(yōu)化的參數(shù)语淘,或有特別的情況。
        // zerolatency: 零延遲际歼,用在需要非常低的延遲的情況下惶翻,比如視頻直播的編碼
        av_dict_set(&param, "tune", "zerolatency", 0);
    }
    
    // 11.輸出打印信息,內(nèi)部是通過printf函數(shù)輸出(不需要輸出可以注釋掉該局)
    av_dump_format(pFormatCtx, 0, out_file, 1);
    
    // 12.通過 codec_id 找到對(duì)應(yīng)的編碼器
    pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
    if (!pCodec) {
        printf("Can not find encoder! \n");
        return -1;
    }
    
    // 13.打開編碼器鹅心,并設(shè)置參數(shù) param
    if (avcodec_open2(pCodecCtx, pCodec,&param) < 0) {
        printf("Failed to open encoder! \n");
        return -1;
    }
    
    // 13.初始化原始數(shù)據(jù)對(duì)象: AVFrame
    pFrame = av_frame_alloc();
    
    // 14.通過像素格式(這里為 YUV)獲取圖片的真實(shí)大小吕粗,例如將 480 * 720 轉(zhuǎn)換成 int 類型
    avpicture_fill((AVPicture *)pFrame, picture_buf, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height);
    
    // 15.h264 封裝格式的文件頭部,基本上每種編碼都有著自己的格式的頭部旭愧,想看具體實(shí)現(xiàn)的同學(xué)可以看看 h264 的具體實(shí)現(xiàn)
    avformat_write_header(pFormatCtx, NULL);
    
    // 16.創(chuàng)建編碼后的數(shù)據(jù) AVPacket 結(jié)構(gòu)體來存儲(chǔ) AVFrame 編碼后生成的數(shù)據(jù)
    av_new_packet(&pkt, picture_size);
    
    // 17.設(shè)置 yuv 數(shù)據(jù)中 y 圖的寬高
    y_size = pCodecCtx->width * pCodecCtx->height;
    
    return 0;
}

/*
 * 將CMSampleBufferRef格式的數(shù)據(jù)編碼成h264并寫入文件
 * 
 */
- (void)encoderToH264:(CMSampleBufferRef)sampleBuffer
{
    // 1.通過CMSampleBufferRef對(duì)象獲取CVPixelBufferRef對(duì)象
    CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    
    // 2.鎖定imageBuffer內(nèi)存地址開始進(jìn)行編碼
    if (CVPixelBufferLockBaseAddress(imageBuffer, 0) == kCVReturnSuccess) {
        // 3.從CVPixelBufferRef讀取YUV的值
        // NV12和NV21屬于YUV格式颅筋,是一種two-plane模式,即Y和UV分為兩個(gè)Plane输枯,但是UV(CbCr)為交錯(cuò)存儲(chǔ)议泵,而不是分為三個(gè)plane
        // 3.1.獲取Y分量的地址
        UInt8 *bufferPtr = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,0);
        // 3.2.獲取UV分量的地址
        UInt8 *bufferPtr1 = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,1);
        
        // 3.3.根據(jù)像素獲取圖片的真實(shí)寬度&高度
        size_t width = CVPixelBufferGetWidth(imageBuffer);
        size_t height = CVPixelBufferGetHeight(imageBuffer);
        // 獲取Y分量長(zhǎng)度
        size_t bytesrow0 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,0);
        size_t bytesrow1  = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,1);
        UInt8 *yuv420_data = (UInt8 *)malloc(width * height *3/2);
        
        // 3.4.將NV12數(shù)據(jù)轉(zhuǎn)成YUV420P(I420)數(shù)據(jù)
        UInt8 *pY = bufferPtr ;
        UInt8 *pUV = bufferPtr1;
        UInt8 *pU = yuv420_data + width*height;
        UInt8 *pV = pU + width*height/4;
        for(int i =0;i<height;i++)
        {
            memcpy(yuv420_data+i*width,pY+i*bytesrow0,width);
        }
        for(int j = 0;j<height/2;j++)
        {
            for(int i =0;i<width/2;i++)
            {
                *(pU++) = pUV[i<<1];
                *(pV++) = pUV[(i<<1) + 1];
            }
            pUV+=bytesrow1;
        }
        
        // 3.5.分別讀取YUV的數(shù)據(jù)
        picture_buf = yuv420_data;
        pFrame->data[0] = picture_buf;              // Y
        pFrame->data[1] = picture_buf+ y_size;      // U
        pFrame->data[2] = picture_buf+ y_size*5/4;  // V
        
        // 4.設(shè)置當(dāng)前幀
        pFrame->pts = framecnt;
        int got_picture = 0;
        
        // 4.設(shè)置寬度高度以及YUV各式
        pFrame->width = encoder_h264_frame_width;
        pFrame->height = encoder_h264_frame_height;
        pFrame->format = PIX_FMT_YUV420P;
        
        // 5.對(duì)編碼前的原始數(shù)據(jù)(AVFormat)利用編碼器進(jìn)行編碼,將 pFrame 編碼后的數(shù)據(jù)傳入pkt 中
        int ret = avcodec_encode_video2(pCodecCtx, &pkt, pFrame, &got_picture);
        if(ret < 0) {
            printf("Failed to encode! \n");
            
        }
        
        // 6.編碼成功后寫入 AVPacket 到 輸入輸出數(shù)據(jù)操作著 pFormatCtx 中桃熄,當(dāng)然先口,記得釋放內(nèi)存
        if (got_picture==1) {
            framecnt++;
            pkt.stream_index = video_st->index;
            ret = av_write_frame(pFormatCtx, &pkt);
            av_free_packet(&pkt);
        }
        
        // 7.釋放yuv數(shù)據(jù)
        free(yuv420_data);
    }
    
    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
}


/*
 * 釋放資源
 */
- (void)freeX264Resource
{
    // 1.釋放AVFormatContext
    int ret = flush_encoder(pFormatCtx,0);
    if (ret < 0) {
        printf("Flushing encoder failed\n");
    }
    
    // 2.將還未輸出的AVPacket輸出出來
    av_write_trailer(pFormatCtx);
    
    // 3.關(guān)閉資源
    if (video_st){
        avcodec_close(video_st->codec);
        av_free(pFrame);
    }
    avio_close(pFormatCtx->pb);
    avformat_free_context(pFormatCtx);
}

int flush_encoder(AVFormatContext *fmt_ctx,unsigned int stream_index)
{
    int ret;
    int got_frame;
    AVPacket enc_pkt;
    if (!(fmt_ctx->streams[stream_index]->codec->codec->capabilities &
          CODEC_CAP_DELAY))
        return 0;
    
    while (1) {
        enc_pkt.data = NULL;
        enc_pkt.size = 0;
        av_init_packet(&enc_pkt);
        ret = avcodec_encode_video2 (fmt_ctx->streams[stream_index]->codec, &enc_pkt,
                                     NULL, &got_frame);
        av_frame_free(NULL);
        if (ret < 0)
            break;
        if (!got_frame){
            ret=0;
            break;
        }
        ret = av_write_frame(fmt_ctx, &enc_pkt);
        if (ret < 0)
            break;
    }
    return ret;
}
@end

外面調(diào)用:

#import "ViewController.h"
#import <GPUImage/GPUImage.h>
#import <AVFoundation/AVFoundation.h>
#import "X264Manager.h"

@interface ViewController ()<GPUImageVideoCameraDelegate>
// 創(chuàng)建攝像頭
@property (strong, nonatomic) GPUImageVideoCamera *camera;
@property (strong, nonatomic) GPUImageView *previewLayer;
// 創(chuàng)建幾個(gè)濾鏡
/**
 摩皮
 */
@property (strong, nonatomic) GPUImageBilateralFilter *bilaterFilter;
/**
 曝光
 */
@property (strong, nonatomic) GPUImageExposureFilter *exposureFilter;
/**
 美白
 */
@property (strong, nonatomic) GPUImageBrightnessFilter *brigtnessFilter;
/**
 飽和
 */
@property (strong, nonatomic) GPUImageSaturationFilter *saturationFilter;

@property (strong, nonatomic) X264Manager *encoder;

@end

@implementation ViewController
-(GPUImageVideoCamera *)camera {
    if (!_camera) {
        _camera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPresetHigh cameraPosition:AVCaptureDevicePositionFront];
    }
    return _camera;
}
-(GPUImageView *)previewLayer {
    if (!_previewLayer) {
        _previewLayer = [[GPUImageView alloc] initWithFrame:self.view.bounds];
    }
    return _previewLayer;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    self.encoder = [[X264Manager alloc] init];
    // 1.獲取沙盒路徑
    NSString *file = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"abc.h264"];
    // 2.開始編碼
    [self.encoder setFileSavedPath:file];
    // 特別注意: 寬度&高度
    [self.encoder setX264ResourceWithVideoWidth:720 height:1280 bitrate:1024*800];
    
    
    // 初始化一些濾鏡
    self.bilaterFilter = [[GPUImageBilateralFilter alloc] init];
    self.exposureFilter = [[GPUImageExposureFilter alloc] init];
    self.brigtnessFilter = [[GPUImageBrightnessFilter alloc] init];
    self.saturationFilter = [[GPUImageSaturationFilter alloc] init];
    // 調(diào)整攝像頭的方向
    //    self.camera.outputImageOrientation = UIInterfaceOrientationPortrait;
    // 設(shè)置豎屏 否則露出來的視頻 和我們想要的不一樣
    self.camera.videoCaptureConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
    // 調(diào)整攝像頭的鏡像 自己動(dòng)的方向和鏡子中的方向一致
    self.camera.videoCaptureConnection.videoMirrored = YES;
    self.camera.captureSession.sessionPreset = AVCaptureSessionPresetHigh;
    //    self.camera.horizontallyMirrorFrontFacingCamera = YES;
    // 創(chuàng)建過濾層
    GPUImageFilterGroup *filterGroup = [self obtainFilterGroup];
    [self.camera addTarget:filterGroup];
    // 將imageview 添加到過濾層上
    [filterGroup addTarget:self.previewLayer];
    [self.view insertSubview:self.previewLayer atIndex:0];
    self.camera.delegate = self;
    // 開始拍攝
    [self.camera startCameraCapture];
    
}
/**
 創(chuàng)建過濾組
 */
- (GPUImageFilterGroup *)obtainFilterGroup{
    
    GPUImageFilterGroup *group = [[GPUImageFilterGroup alloc] init];
    // 按照順序組成一個(gè)鏈
    [self.bilaterFilter addTarget:self.exposureFilter];
    [self.exposureFilter addTarget:self.brigtnessFilter];
    [self.brigtnessFilter addTarget:self.saturationFilter];
    // 將濾鏡添加到濾鏡組中(開始和結(jié)尾)
    group.initialFilters = @[self.bilaterFilter];
    group.terminalFilter = self.saturationFilter;
    
    return group;
}
/**
 結(jié)束直播相關(guān)的事件
 
 @param sender 按鈕
 */
- (IBAction)endLiveAction:(UIButton *)sender {
    
    [self.camera stopCameraCapture];
    [self.previewLayer removeFromSuperview];
    [self.encoder freeX264Resource];

}
#pragma mark - camera 的 delegate

- (void)willOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer{
    NSLog(@"---------------");
    [self.encoder encoderToH264:sampleBuffer];
}
@end

如果ffmpeg+x264打包不成功的 可以找我要 我直接發(fā)給你完整的包,加我qq:2586784401 瞳收,添加留言:ffmpeg+x264 碉京,否則不理哈。
值得注意的是軟編碼這塊

 // 特別注意: 寬度&高度
    [self.encoder setX264ResourceWithVideoWidth:720 height:1280 bitrate:1024*800];

因?yàn)橐曨l我們是豎屏錄制一般螟深,所以寬度和高度應(yīng)該和分辨率的相反谐宙。
軟編碼中的yuv:

yuv是什么?

  • 視頻是由一幀一幀的數(shù)據(jù)連接而成界弧,而一幀視頻數(shù)據(jù)其實(shí)就是一張圖片凡蜻。
    yuv是一種圖片儲(chǔ)存格式搭综,跟RGB格式類似。
    RGB格式的圖片很好理解咽瓷,計(jì)算機(jī)中的大多數(shù)圖片设凹,都是以RGB格式存儲(chǔ)的。
  • yuv中茅姜,y表示亮度,單獨(dú)只有y數(shù)據(jù)就可以形成一張圖片月匣,只不過這張圖片是灰色的钻洒。u和v表示色差(u和v也被稱為:Cb-藍(lán)色差,Cr-紅色差)
  • 為什么要yuv锄开?
    有一定歷史原因素标,最早的電視信號(hào),為了兼容黑白電視萍悴,采用的就是yuv格式头遭。
    一張yuv的圖像,去掉uv癣诱,只保留y计维,這張圖片就是黑白的。
    而且yuv可以通過拋棄色差來進(jìn)行帶寬優(yōu)化撕予。
    比如yuv420格式圖像相比RGB來說鲫惶,要節(jié)省一半的字節(jié)大小,拋棄相鄰的色差對(duì)于人眼來說实抡,差別不大欠母。

YUV顏色存儲(chǔ)格式

  • 常用的I420(YUV420P),NV12(YUV420SP),YV12(YUV420P),NV21(YUV420SP)等都是屬于YUV420,NV12是一種兩平面存儲(chǔ)方式吆寨,Y為一個(gè)平面赏淌,交錯(cuò)的UV為另一個(gè)平面
  • 通常,用來遠(yuǎn)程傳輸?shù)氖荌420數(shù)據(jù)啄清,而本地?cái)z像頭采集的是NV12數(shù)據(jù)六水。(iOS)
    所有在真正編碼的過程中, 需要將NV12數(shù)據(jù)轉(zhuǎn)成I420數(shù)據(jù)進(jìn)行編碼
  • 編碼方式完成之后需要研究傳輸協(xié)議,其中傳輸協(xié)議最常見的就是rtmp 和
    hls

最常見的協(xié)議有rtmp協(xié)議和hls協(xié)議

  • rtmp協(xié)議:
    RTMP協(xié)議是 Adobe 公司開發(fā)的一個(gè)基于TCP的應(yīng)用層協(xié)議盒延,Adobe 公司也公布了關(guān)于RTMP的規(guī)范
    1.1 它的優(yōu)點(diǎn):
    實(shí)時(shí)性高:一般它的延遲在3秒左右缩擂,正常的可能在5秒左右,一般實(shí)時(shí)行高用的都是rtmp
    支持加密:加密方式:rtmpe 和rtmps為加密方式添寺。
    穩(wěn)定行高:http雖然也穩(wěn)定胯盯,但是穩(wěn)定性不至在服務(wù)器上還有在cdn分發(fā)上,rtmp都能很好的支持计露。
    1.2 rtmp的原理:
    rtmp也需要和服務(wù)器通過“握手”基于傳輸層建立基于rtmp協(xié)議的鏈接博脑。
    rtmp在傳輸?shù)臅r(shí)候會(huì)做自己的格式化憎乙,這種格式化我們稱之為:rtmp Message
    rtmp為了實(shí)現(xiàn)多路復(fù)用,公平性等等叉趣,它一般在發(fā)送方會(huì)組裝消息的頭里面封裝消息的id的chunk泞边,消息體的數(shù)據(jù)長(zhǎng)度等等 ,會(huì)發(fā)送一個(gè)或者部分完整包給接收方疗杉,接收方在收到消息時(shí)進(jìn)行還原(個(gè)人覺得有點(diǎn)像我們自己定義的socket協(xié)議)阵谚。
  • hls:
    1.1 HTTP Live Streaming(HLS)是蘋果公司實(shí)現(xiàn)的基于HTTP的流媒體傳輸協(xié)議,可實(shí)現(xiàn)流媒體的直播和點(diǎn)播烟具。原理上是將視頻流分片成一系列HTTP下載文件梢什。所以,HLS比RTMP有較高的延遲朝聋。HLS基于HTTP協(xié)議實(shí)現(xiàn)嗡午,傳輸內(nèi)容包括兩部分,一是M3U8描述文件冀痕,二是TS媒體文件
    相對(duì)于常見的流媒體直播協(xié)議荔睹,例如RTMP協(xié)議、RTSP協(xié)議言蛇、MMS協(xié)議等僻他,HLS直播最大的不同在于,直播客戶端獲取到的猜极,并不是一個(gè)完整的數(shù)據(jù)流中姜。HLS協(xié)議在服務(wù)器端將直播數(shù)據(jù)流存儲(chǔ)為連續(xù)的、很短時(shí)長(zhǎng)的媒體文件(MPEG-TS格式)跟伏,而客戶端則不斷的下載并播放這些小文件丢胚,因?yàn)榉?wù)器端總是會(huì)將最新的直播數(shù)據(jù)生成新的小文件,這樣客戶端只要不停的按順序播放從服務(wù)器獲取到的文件受扳,就實(shí)現(xiàn)了直播携龟。
    由此可見,基本上可以認(rèn)為勘高,HLS是以點(diǎn)播的技術(shù)方式來實(shí)現(xiàn)直播峡蟋。
    1.2 hls 實(shí)現(xiàn)原理:
    采集數(shù)據(jù)
    對(duì)原始數(shù)據(jù)進(jìn)行h264編碼和aac編碼
    視頻或者音頻封裝成mpeg-ts包
    hls分段生成策略以及m3u8索引文件
    http傳輸協(xié)議進(jìn)行傳輸數(shù)據(jù)
    1.3 用一張圖片進(jìn)行解釋:


    image.png

    這個(gè)命令可以把視頻 切成一個(gè)個(gè)m3u8的索引文件
    ffmpeg -i XXX.mp4 -c:v libx264 -c:a copy -f hls XXX.m3u8
    總結(jié):
    一般實(shí)時(shí)性較高的一般使用rtmp協(xié)議,對(duì)實(shí)時(shí)性要求不高可以使用hls協(xié)議华望。一般來講我們做直播用的都是rtmp協(xié)議蕊蝗。
    我們知道了傳輸協(xié)議了 但是對(duì)于我們普通人而言或者做過一點(diǎn)點(diǎn)直播人而言實(shí)現(xiàn)推流還是比較難的,因?yàn)檫@里面需要做很多工作赖舟,(比如你需要知道rtmp協(xié)議具體每一個(gè)小步怎么做的蓬戚、怎么進(jìn)行推送等等),但是在it中就是大牛很多 而且他們一般都不怎么說話宾抓,對(duì)于oc 而言 現(xiàn)在用到的推流框架最好的就是這個(gè)LFLiveKit子漩,它不僅可以推rtmp 也可以推送 hls協(xié)議的豫喧,星目前3000多個(gè) ,其中它還有實(shí)現(xiàn)美顏相機(jī)的部分幢泼,個(gè)人對(duì)它的一點(diǎn)點(diǎn)不滿就是覺得它美顏相機(jī)可調(diào)式美顏功能太少 紧显,你要做類似于斗魚那樣設(shè)置 曝光、磨皮缕棵、美顏孵班、飽和度等等的 你需要fork一份到自己的倉(cāng)庫(kù)中 然后進(jìn)行修改美顏相機(jī)那個(gè)文件,默認(rèn)的好像只有美顏 和 飽和度它的這個(gè)框架挥吵,目前發(fā)現(xiàn)它已經(jīng)兩年沒更新了 不用怕 一般這種都是底層的東西 變化不大重父。
    下面是我簡(jiǎn)單使用,說實(shí)話用了這個(gè)框架自己真的是太省心了:

#import "ViewController.h"
#import <LFLiveKit/LFLiveKit.h>


@interface ViewController ()<LFLiveSessionDelegate>

@property (strong, nonatomic) LFLiveSession *session;


@end

@implementation ViewController
- (LFLiveSession*)session {
    if (!_session) {
        LFLiveVideoConfiguration * liveConfigation = [LFLiveVideoConfiguration defaultConfigurationForQuality:LFLiveVideoQuality_High2 outputImageOrientation:UIInterfaceOrientationPortrait];
        _session = [[LFLiveSession alloc] initWithAudioConfiguration:[LFLiveAudioConfiguration defaultConfiguration] videoConfiguration:liveConfigation];
        _session.preView = self.view;
        _session.delegate = self;
    }
    return _session;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    [self stopLive];
    // 是否需要美顏
    self.session.beautyFace = NO;
}
- (IBAction)startLive {
    LFLiveStreamInfo *streamInfo = [LFLiveStreamInfo new];
    //類似于: rtmp://localhost:1935/rtmplive/home
    streamInfo.url = @"推流地址";
    [self.session startLive:streamInfo];
    self.session.running = YES;
}
- (IBAction)switchCamera:(UIButton *)sender {
    if (self.session.captureDevicePosition == 1) {
        self.session.captureDevicePosition = 2;
    }else{
        self.session.captureDevicePosition = 1;
    }
}

- (IBAction)stopLive {
    [self.session stopLive];
}
- (IBAction)beauty:(UISlider *)sender {
    self.session.beautyLevel = sender.value;
}
- (IBAction)bright:(UISlider *)sender {
    self.session.brightLevel = sender.value;
}
- (IBAction)zoom:(UISlider *)sender {
    self.session.zoomScale = sender.value;
}
#pragma mark - delegate
/** live status changed will callback */
- (void)liveSession:(nullable LFLiveSession *)session liveStateDidChange:(LFLiveState)state{
    NSLog(@"LFLiveState -- %zd",state);
}
/** live debug info callback */
- (void)liveSession:(nullable LFLiveSession *)session debugInfo:(nullable LFLiveDebug *)debugInfo{
    NSLog(@"LFLiveDebug -- %@",debugInfo);
}
/** callback socket errorcode */
- (void)liveSession:(nullable LFLiveSession *)session errorCode:(LFLiveSocketErrorCode)errorCode{
    NSLog(@"LFLiveSocketErrorCode -- %zd",errorCode);
}
@end

其中注意一個(gè)地方 忽匈,如果你按照官方文檔寫 是錄制不了視頻的,必須加上一句話(作者官方文檔中是沒有說的矿辽,猜了好久丹允,不知道自己二還是作者馬虎)

self.session.running = YES;

還需要注意一個(gè)地方:一般來說如果我們沒有做過直播項(xiàng)目 我們就需要自己搭建本地的nginx 和 rtmp協(xié)議的服務(wù)器,用我們自己的電腦袋倔。(自己百度一下雕蔽,我也是遇到好多錯(cuò)誤后來解決了)

  • 推流到服務(wù)器,服務(wù)器進(jìn)行流分發(fā) 其他用戶的手機(jī)就會(huì)對(duì)視頻進(jìn)行解碼宾娜,一般來說解碼的工作量更加的巨大批狐,我們一般人解碼也不太現(xiàn)實(shí),我們也是用的框架前塔,最常用的框架就是:ijkplayer嚣艇,這個(gè)是b站進(jìn)行開源的項(xiàng)目,也是純c的华弓,但是打包我們自己的ios項(xiàng)目
    需要進(jìn)行如下的操作:
第一步:
# install homebrew, git, yasm
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew install git
brew install yasm

# add these lines to your ~/.bash_profile or ~/.profile
# export ANDROID_SDK=<your sdk path>
# export ANDROID_NDK=<your ndk path>

# on Cygwin (unmaintained)
# install git, make, yasm
第二部:
git clone https://github.com/Bilibili/ijkplayer.git ijkplayer-ios
cd ijkplayer-ios
git checkout -B latest k0.8.8

./init-ios.sh

cd ios
./compile-ffmpeg.sh clean
./compile-ffmpeg.sh all

# Demo
#     open ios/IJKMediaDemo/IJKMediaDemo.xcodeproj with Xcode
# 
# Import into Your own Application
#     Select your project in Xcode.
#     File -> Add Files to ... -> Select ios/IJKMediaPlayer/IJKMediaPlayer.xcodeproj
#     Select your Application's target.
#     Build Phases -> Target Dependencies -> Select IJKMediaFramework
#     Build Phases -> Link Binary with Libraries -> Add:
#         IJKMediaFramework.framework
#
#         AudioToolbox.framework
#         AVFoundation.framework
#         CoreGraphics.framework
#         CoreMedia.framework
#         CoreVideo.framework
#         libbz2.tbd
#         libz.tbd
#         MediaPlayer.framework
#         MobileCoreServices.framework
#         OpenGLES.framework
#         QuartzCore.framework
#         UIKit.framework
#         VideoToolbox.framework
#
#         ... (Maybe something else, if you get any link error)
# 

最后你需要編譯模擬器和真機(jī)的包食零,打成一個(gè)靜態(tài)庫(kù),(怎么打包一個(gè)framework不會(huì)的自己百度一下寂屏,這里不說了)
不得不說我shell方面不怎么樣贰谣,我按照它一步一步執(zhí)行的 但是確實(shí)還是不成功,最后包一個(gè)這個(gè)錯(cuò)誤:

AR  libavfilter/libavfilter.a
AR  libavformat/libavformat.a
AS  libavcodec/arm/aacpsdsp_neon.o
CC  libavcodec/arm/blockdsp_init_neon.o
./libavutil/arm/asm.S:50:9: error: unknown directive
        .arch armv7-a
        ^

打包不成功迁霎,按照https://www.colabug.com/2876591.html這篇文章就可以了吱抚,原來是xcode9.3 對(duì)arm7支持變?nèi)趿耍绻愕膞code<= 9.1 按照官網(wǎng)執(zhí)行應(yīng)該沒問題考廉。
下面是我的解碼代碼秘豹,其中它會(huì)有不在主線程的錯(cuò)誤,因?yàn)樗陌蟾攀且荒昵拔矣X得可能是ijkplayer還沒有解決這個(gè)問題 我確定不是自身問題芝此。這個(gè)是ijkplayer自身的問題 因?yàn)閤code更新了憋肖,他代碼一年多沒更新了因痛,很多需要放到主線程的 他沒有這么操作,當(dāng)時(shí)可能沒問題對(duì)于他來說岸更,現(xiàn)在有問題了等待他解決吧鸵膏。
解碼代碼:

 // 設(shè)置一些基本的參數(shù)
    IJKFFOptions *options = [IJKFFOptions optionsByDefault];
    // 讓其支持硬編碼,默認(rèn)支持軟編碼
    [options setOptionValue:@"1" forKey:@"videotoolbox" ofCategory:kIJKFFOptionCategoryPlayer];
    // 初始化
    IJKFFMoviePlayerController *moviePlayer = [[IJKFFMoviePlayerController alloc] initWithContentURL:[NSURL URLWithString:liveStr] withOptions:options];
    if (self.authorModel.push == 1) { //手機(jī)拍攝
        moviePlayer.view.frame = CGRectMake(0, 120, self.view.frame.size.width, self.view.frame.size.width * 3/4);
    }else {
        moviePlayer.view.frame = self.view.bounds;
    }
    [self.view insertSubview:moviePlayer.view atIndex:0];
    // 開始播放
    [moviePlayer prepareToPlay];
    self.moviePlayer = moviePlayer;

最后發(fā)現(xiàn)我們做一個(gè)直播的項(xiàng)目 其實(shí)就是使用LFLiveKit和ijkplayer怎炊,實(shí)現(xiàn)一個(gè)推流和解視頻組裝視頻流谭企,但是我們學(xué)知識(shí)不能滿足怎么實(shí)現(xiàn)就得了 我們需要知道它的原理,如果我們沒有學(xué)習(xí)GPUImage 美顏相機(jī)加一點(diǎn)功能也就不會(huì)了评肆,如果不學(xué)習(xí)硬編碼和軟編碼也就不知道個(gè)別參數(shù)代表的意思债查,如果以后修改也就不會(huì)了,如果別人問你yuv 瓜挽、rtmp盹廷、hls、h264這些等等你可能就不知道了久橙,這些內(nèi)容我大概花了一個(gè)多月的時(shí)間研究出來的俄占,如果有不對(duì)的希望大家指正,大家如果覺得我哪塊胡說也可以罵我淆衷,因?yàn)楫吘棺约哄e(cuò)了嘛缸榄,我會(huì)好好接受你的建議并改正。其實(shí)視頻編碼解碼這塊遠(yuǎn)遠(yuǎn)不止于此祝拯,要想做的好你需要對(duì)視頻的編碼和解碼很了解還有對(duì)底層的c語言很懂才行甚带,這個(gè)過程可能需要花幾年的時(shí)間。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末佳头,一起剝皮案震驚了整個(gè)濱河市鹰贵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌畜晰,老刑警劉巖砾莱,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異凄鼻,居然都是意外死亡腊瑟,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門块蚌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來闰非,“玉大人,你說我怎么就攤上這事峭范〔扑桑” “怎么了?”我有些...
    開封第一講書人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)辆毡。 經(jīng)常有香客問我菜秦,道長(zhǎng),這世上最難降的妖魔是什么舶掖? 我笑而不...
    開封第一講書人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任球昨,我火速辦了婚禮,結(jié)果婚禮上眨攘,老公的妹妹穿的比我還像新娘主慰。我一直安慰自己,他們只是感情好鲫售,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開白布共螺。 她就那樣靜靜地躺著,像睡著了一般情竹。 火紅的嫁衣襯著肌膚如雪藐不。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評(píng)論 1 299
  • 那天秦效,我揣著相機(jī)與錄音佳吞,去河邊找鬼。 笑死棉安,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的铸抑。 我是一名探鬼主播贡耽,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼鹊汛!你這毒婦竟也來了蒲赂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤刁憋,失蹤者是張志新(化名)和其女友劉穎滥嘴,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體至耻,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡若皱,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了尘颓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片走触。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖疤苹,靈堂內(nèi)的尸體忽然破棺而出互广,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布惫皱,位于F島的核電站像樊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏旅敷。R本人自食惡果不足惜生棍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望扫皱。 院中可真熱鬧足绅,春花似錦、人聲如沸韩脑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽段多。三九已至首量,卻和暖如春新啼,著一層夾襖步出監(jiān)牢的瞬間磕蒲,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工霹娄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留觉啊,地道東北人拣宏。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像杠人,于是被迫代替她去往敵國(guó)和親勋乾。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • ### YUV顏色空間 視頻是由一幀一幀的數(shù)據(jù)連接而成嗡善,而一幀視頻數(shù)據(jù)其實(shí)就是一張圖片辑莫。 yuv是一種圖片儲(chǔ)存格式...
    天使君閱讀 3,282評(píng)論 0 4
  • 視頻 視頻實(shí)質(zhì):純粹的視頻(不包括音頻)實(shí)質(zhì)上就是一組幀圖片,經(jīng)過視頻編碼成為視頻(video)文件再把音頻(au...
    勇敢的_心_閱讀 2,912評(píng)論 1 30
  • 要理解RTMP推流罩引,我們就要知道詳細(xì)原理各吨,這方面的文章有很多,我也看到過學(xué)習(xí)過很多這樣的文章袁铐,但是很多都沒有詳細(xì)的...
    RiemannLee_22dc閱讀 11,512評(píng)論 21 31
  • 前言:每個(gè)成功者多是站在巨人的肩膀上揭蜒!在做直播開發(fā)時(shí) 碰到了很多問題,在收集了許多人博客的基礎(chǔ)上做出來了成功的直播...
    IM魂影閱讀 1,982評(píng)論 1 12
  • 詠蘭 無人知曉清幽處昭躺,依泉伴雨蘭草疏忌锯; 待得端陽重五月,清風(fēng)送暖暗香徐领炫。 踏青 重五踏青艾蒿蔓偶垮,道是故里蝶影單; ...
    劉海峰閱讀 582評(píng)論 1 1