35

iOS解码关于视频中带B帧排序问题

 5 years ago
source link: https://www.tuicool.com/articles/7j2IZ3b
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

需求

在解码带B帧的视频编码码流中,由于B帧的存在,导致parse出来的数据时间戳本身不是连续的,因为B帧需要可能需要参考后面的帧.因此,解码完成后需要对码流解码做一个重排序

实现原理

首先分析parse出来的数据的时间戳,后面得出的结论是除了第一帧时间戳为0,总体时间戳是增长的,后面每4帧为一组,每组中的顺序需要重新排序,排序后即可实现时间戳连续递增.

考虑内存开销,我们每次缓存4帧视频数据,将其放入线性表中,然后使用排序算法对这4帧数据根据pts做一个排序,排好序后即可传给渲染模块将其渲染到屏幕上.

注意: 使用FFmpeg硬解无需关心排序问题,因为FFmpeg内部有缓存排序机制,即从FFmepg拿到解码后的数据可直接渲染.

阅读前提

代码地址 : iOS解码关于视频中带B帧排序问题

掘金地址 : iOS解码关于视频中带B帧排序问题

简书地址 : iOS解码关于视频中带B帧排序问题

博客地址 : iOS解码关于视频中带B帧排序问题

TODO

因为我们无法判断码流中的B帧是否依赖后面的视频帧,而是 经过分析可得视频帧的时间戳是总体增长,每组4帧,需要对4帧数据进行排序,目前暂时未知是否有非4帧一组的视频帧,如果有则此Demo会有问题,因此如果有了解此方面大神望评论告知更好的解决方案.

总体架构

本文基于已实现解码一个包含带B帧的H.265码流文件.因此,这里不对parse,解码,渲染做过多说明,如需了解参考阅读前提中的链接.

下面是从FFmpeg parse H.265文件得到的时间戳,我们可以看到,parse出来的时间戳总体是递增的,除了第一帧时间戳为0的数据外,剩下的时间戳均以4个一组,每组内时间戳不是递增的,因为解码时我们仍需按照Parse出来的数据送去解码,所以,我们只能对解码后的数据做一个排序,即将每组数据装入一个线性表,然后使用任一一种排序算法对其排序.

+0800 XDXVideoDecoder[1489:223381] Test - 0.000000

2019-06-25 12:38:09.656282+0800 XDXVideoDecoder[1489:223381] Test - 0.133333
2019-06-25 12:38:09.659350+0800 XDXVideoDecoder[1489:223381] Test - 0.066667
2019-06-25 12:38:09.660900+0800 XDXVideoDecoder[1489:223381] Test - 0.033333
2019-06-25 12:38:09.662889+0800 XDXVideoDecoder[1489:223381] Test - 0.100000


2019-06-25 12:38:09.664786+0800 XDXVideoDecoder[1489:223381] Test - 0.266667
2019-06-25 12:38:09.666900+0800 XDXVideoDecoder[1489:223381] Test - 0.200000
2019-06-25 12:38:09.668196+0800 XDXVideoDecoder[1489:223381] Test - 0.166667
2019-06-25 12:38:09.669825+0800 XDXVideoDecoder[1489:223381] Test - 0.233333


2019-06-25 12:38:09.670368+0800 XDXVideoDecoder[1489:223381] Test - 0.400000
2019-06-25 12:38:09.670948+0800 XDXVideoDecoder[1489:223381] Test - 0.333333
2019-06-25 12:38:09.671806+0800 XDXVideoDecoder[1489:223381] Test - 0.300000
2019-06-25 12:38:09.673082+0800 XDXVideoDecoder[1489:223381] Test - 0.366667


2019-06-25 12:38:09.673899+0800 XDXVideoDecoder[1489:223381] Test - 0.533333
2019-06-25 12:38:09.674961+0800 XDXVideoDecoder[1489:223381] Test - 0.466667
2019-06-25 12:38:09.675637+0800 XDXVideoDecoder[1489:223381] Test - 0.433333
2019-06-25 12:38:09.676451+0800 XDXVideoDecoder[1489:223381] Test - 0.500000

快速使用

  • 初始化

    - (void)viewDidLoad {
        self.sortHandler = [[XDXSortFrameHandler alloc] init];
        self.sortHandler.delegate = self;
    }
    
  • 在解码回调中将解码出来数据送给排序模块

- (void)getVideoDecodeDataCallback:(CMSampleBufferRef)sampleBuffer isFirstFrame:(BOOL)isFirstFrame {
    if (self.isH265File) {
        // Note : the first frame not need to sort.
        if (isFirstFrame) {
            CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer);
            [self.previewView displayPixelBuffer:pix];
            return;
        }
        
        [self.sortHandler addDataToLinkList:sampleBuffer];
    }else {
        CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer);
        [self.previewView displayPixelBuffer:pix];
    }
}
  • 排序后通过代理方法拿到排过序的每帧数据将其渲染到屏幕
    - (void)getSortedVideoNode:(CMSampleBufferRef)sampleBuffer {
        int64_t pts = (int64_t)(CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) * 1000);
        static int64_t lastpts = 0;
        NSLog(@"Test marigin - %lld",pts - lastpts);
        lastpts = pts;
        
        [self.previewView displayPixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer)];
    }
    

具体实现

1. 定义一个线性表用来缓存视频帧

使用一个装有 CMSampleBufferRef 指针的数据来缓存视频帧.因为每4帧排一次序,所以长度为4,使用索引Index记录每个视频帧的位置.

const static int g_maxSize = 4;

struct XDXSortLinkList {
    CMSampleBufferRef dataArray[g_maxSize];
    int index;
};

2. 初始化

@interface XDXSortFrameHandler ()
{
    XDXSortLinkList _sortLinkList;
}

......

- (instancetype)init {
    if (self = [super init]) {
        XDXSortLinkList linkList = {
            .index = 0,
            .dataArray = {0},
        };
        
        _sortLinkList = linkList;
    }
    return self;
}

3.将视频帧存入线性表

CMSampleBufferRef 实际即为一个指针,首先使用 CFRetain 函数使其引用计数+1以维持我们使用期间视频帧不会被销毁,当线性表没有装满前一直为其添加视频帧,当线性表装满后使用插入排序对其进行排序,排好后将数据依次以代理方法的形式传出以供渲染模块使用.使用完成后使用 CFRelease 减少原始数据的引用计数.

- (void)addDataToLinkList:(CMSampleBufferRef)sampleBufferRef {
    CFRetain(sampleBufferRef);
    _sortLinkList.dataArray[_sortLinkList.index] = sampleBufferRef;
    _sortLinkList.index++;
    
    if (_sortLinkList.index == g_maxSize) {
        _sortLinkList.index = 0;
        
        // sort
        [self selectSortWithLinkList:&_sortLinkList];
        
        for (int i = 0; i < g_maxSize; i++) {
            if ([self.delegate respondsToSelector:@selector(getSortedVideoNode:)]) {
                [self.delegate getSortedVideoNode:_sortLinkList.dataArray[i]];
                CFRelease(_sortLinkList.dataArray[i]);
            }
        }   
    }
}

- (void)selectSortWithLinkList:(XDXSortLinkList *)sortLinkList {
    for (int i = 0; i < g_maxSize; i++) {
        int64_t minPTS = i;
        for (int j = i + 1; j < g_maxSize; j++) {
            if ([self getPTS:sortLinkList->dataArray[j]] < [self getPTS:sortLinkList->dataArray[minPTS]]) {
                minPTS = j;
            }
        }
        
        if (i != minPTS) {
            void *tmp = sortLinkList->dataArray[i];
            sortLinkList->dataArray[i] = sortLinkList->dataArray[minPTS];
            sortLinkList->dataArray[minPTS] = tmp;
        }
    }
}
- (int64_t)getPTS:(CMSampleBufferRef)sampleBufferRef {
    int64_t pts = (int64_t)(CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBufferRef)) * 1000);
    return pts;
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK