VTDecompressionSession中的图像缓冲区显示顺序

9
我有一个项目,需要从实时网络流中解码h264视频,并最终得到一个可以在iOS设备上的另一个框架(Unity3D)中显示的纹理。我可以使用VTDecompressionSession成功地解码视频,然后使用CVMetalTextureCacheCreateTextureFromImage(或OpenGL变体)抓取纹理。当我使用低延迟编码器并且图像缓冲区按显示顺序出现时,它工作得很好,但是当我使用常规编码器时,图像缓冲区不按显示顺序出现,重新排序图像缓冲区比我预期的要困难得多。
第一次尝试是使用kVTDecodeFrame_EnableAsynchronousDecompression和kVTDecodeFrame_EnableTemporalProcessing设置VTDecodeFrameFlags。但是,事实证明VTDecompressionSession可以选择忽略标志并做任何它想做的事情。在我的情况下,它选择忽略标志并仍然以编码器顺序输出缓冲区(而不是显示顺序)。基本上是无用的。
下一步尝试是将图像缓冲区与呈现时间戳相关联,并将它们扔进一个向量中,这样当我创建纹理时就可以抓取我需要的图像缓冲区了。问题似乎是进入VTDecompressionSession的图像缓冲区(与时间戳相关联)不再是出现的相同缓冲区,从本质上讲,时间戳无用。
例如,进入解码器...
  VTDecodeFrameFlags flags = kVTDecodeFrame_EnableAsynchronousDecompression;
  VTDecodeInfoFlags flagOut;
  // Presentation time stamp to be passed with the buffer
  NSNumber *nsPts = [NSNumber numberWithDouble:pts];

  VTDecompressionSessionDecodeFrame(_decompressionSession, sampleBuffer, flags,
                                          (void*)CFBridgingRetain(nsPts), &flagOut);

在回调方面...
void decompressionSessionDecodeFrameCallback(void *decompressionOutputRefCon, void *sourceFrameRefCon, OSStatus status, VTDecodeInfoFlags infoFlags, CVImageBufferRef imageBuffer, CMTime presentationTimeStamp, CMTime presentationDuration)
 {
      // The presentation time stamp...
      // No longer seems to be associated with the buffer that it went in with!
      NSNumber* pts = CFBridgingRelease(sourceFrameRefCon);
 }

当订购时,回调端的时间戳以预期速率单调递增,但缓冲区顺序不正确。有人看到我在这里犯了什么错误吗?或者知道如何确定回调端的缓冲区顺序吗?目前为止,我已尝试了几乎我所能想到的一切。


你解决了吗?这个问题让我很头疼。 - jsj
我还没有解决这个问题。我确定,如果不重新排序,包含B帧的视频会按照我预期的高-低-中的顺序播放。然而,很明显,视频帧虽然有高-低-中的排序,但与回调中到达的高-低-中的presentationTimeStamps不再相关联。这破坏了排序,导致奇怪的帧播放顺序。至少我知道我不是唯一一个遇到这个问题的人... - Kaleb
我正在查看XBMC的实现,他们在回调中有一个注释,大致是“有时帧是按解码顺序排列的”,并且他们使用优先级队列来重新排序它们。我必须说我对Video Toolbox API并不是很满意,它的文档很差,这个bug相当严重。 - jsj
我假设你正在使用h264编码...你是使用自己的编码器还是苹果的编码器?我刚刚写了一个测试应用程序,使用苹果编码器和VTDecompressionSession,它可以完美地排序帧。似乎VTDecompressionSession不喜欢我正在使用的编码器的某些东西。 :-/ - Kaleb
这取决于您有多少B帧和P帧。我有几个不同编码器生成的内容,似乎有些比其他更糟糕 - 但是是的,我认为这是B / P帧比率的问题。 - jsj
我发现的问题之一是我的 IDR 帧和随后的 P 和 B 帧关联了不正确的显示时间戳(PTS)。这就解释了为什么回调上的重新排序会出现问题。奇怪的是,这对于 Android 或其他解码器来说并不是问题。 - Kaleb
3个回答

5
在我的情况下,问题不在于VTDecompressionSession,而是demuxer获取了错误的PTS。虽然我无法通过kVTDecodeFrame_EnableAsynchronousDecompression和kVTDecodeFrame_EnableTemporalProcessing标志使VTDecompressionSession按时间(显示)顺序输出帧,但我可以根据PTS使用一个小向量自行进行帧排序。
首先,请确保将所有时间信息与CMSampleBuffer及块缓冲区相关联,以便在VTDecompressionSession回调中接收它。
// Wrap our CMBlockBuffer in a CMSampleBuffer...
CMSampleBufferRef sampleBuffer;

CMTime duration = ...;
CMTime presentationTimeStamp = ...;
CMTime decompressTimeStamp = ...;

CMSampleTimingInfo timingInfo{duration, presentationTimeStamp, decompressTimeStamp};

_sampleTimingArray[0] = timingInfo;
_sampleSizeArray[0] = nalLength;

// Wrap the CMBlockBuffer...
status = CMSampleBufferCreate(kCFAllocatorDefault, blockBuffer, true, NULL, NULL, _formatDescription, 1, 1, _sampleTimingArray, 1, _sampleSizeArray, &sampleBuffer);

接下来,解码这个帧...值得尝试使用标志以显示顺序获取帧。

VTDecodeFrameFlags flags = kVTDecodeFrame_EnableAsynchronousDecompression | kVTDecodeFrame_EnableTemporalProcessing;
VTDecodeInfoFlags flagOut;

VTDecompressionSessionDecodeFrame(_decompressionSession, sampleBuffer, flags,
                                      (void*)CFBridgingRetain(NULL), &flagOut);

在回调方面,我们需要一种对接收到的CVImageBufferRefs进行排序的方法。我使用一个包含CVImageBufferRef和PTS的结构体,然后使用大小为2的向量来进行实际排序。

struct Buffer
{
    CVImageBufferRef imageBuffer = NULL;
    double pts = 0;
};

std::vector <Buffer> _buffer;

我们还需要一种对缓冲区进行排序的方法。始终将数据写入和读取自最低PTS索引的方式效果良好。
 -(int) getMinIndex
 {
     if(_buffer[0].pts > _buffer[1].pts)
     {
         return 1;
     }

     return 0;
 }      

在回调函数中,我们需要用缓冲区填充向量...
 void decompressionSessionDecodeFrameCallback(void *decompressionOutputRefCon, void *sourceFrameRefCon, OSStatus status, VTDecodeInfoFlags infoFlags, CVImageBufferRef imageBuffer, CMTime presentationTimeStamp, CMTime presentationDuration)
 {
    StreamManager *streamManager = (__bridge StreamManager     *)decompressionOutputRefCon;

    @synchronized(streamManager)
    {
    if (status != noErr)
    {
        NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
        NSLog(@"Decompressed error: %@", error);
    }
    else
    {
        // Get the PTS
        double pts = CMTimeGetSeconds(presentationTimeStamp);

        // Fill our buffer initially
        if(!streamManager->_bufferReady)
        {
            Buffer buffer;

            buffer.pts = pts;
            buffer.imageBuffer = imageBuffer;

            CVBufferRetain(buffer.imageBuffer);

            streamManager->_buffer[streamManager->_bufferIndex++] = buffer;
        }
        else
        {
            // Push new buffers to the index with the lowest PTS
            int index = [streamManager getMinIndex];

            // Release the old CVImageBufferRef
            CVBufferRelease(streamManager->_buffer[index].imageBuffer);

            Buffer buffer;

            buffer.pts = pts;
            buffer.imageBuffer = imageBuffer;

            // Retain the new CVImageBufferRef
            CVBufferRetain(buffer.imageBuffer);

            streamManager->_buffer[index] = buffer;
        }

        // Wrap around the buffer when initialized
        // _bufferWindow = 2
        if(streamManager->_bufferIndex == streamManager->_bufferWindow)
        {
            streamManager->_bufferReady = YES;
            streamManager->_bufferIndex = 0;
        }
    }
}
}

最后,我们需要按照时间(显示)顺序排除缓冲区...
 - (void)drainBuffer
 {
      @synchronized(self)
      {
         if(_bufferReady)
         {
             // Drain buffers from the index with the lowest PTS
             int index = [self getMinIndex];

             Buffer buffer = _buffer[index];

             // Do something useful with the buffer now in display order
         }
       }
 }

2
我想稍微改进一下这个答案。虽然提供的解决方案可行,但它需要知道产生输出帧所需的帧数。示例使用缓冲区大小为2,但在我的情况下,我需要缓冲区大小为3。
为了避免事先指定这一点,我们可以利用一个事实,即帧(按显示顺序)在pts / duration方面完全对齐。也就是说,一个帧的结束正好是下一个帧的开始。因此,我们可以简单地累加帧,直到开头没有“间隙”,然后弹出第一个帧,以此类推。此外,我们可以将第一个帧的pts(始终为I-frame)作为初始“头”(因为它不必为零...)。
以下是实现此目的的一些代码:
#include <CoreVideo/CVImageBuffer.h>

#include <boost/container/flat_set.hpp>

inline bool operator<(const CMTime& left, const CMTime& right)
{
    return CMTimeCompare(left, right) < 0;
}

inline bool operator==(const CMTime& left, const CMTime& right)
{
    return CMTimeCompare(left, right) == 0;
}

inline CMTime operator+(const CMTime& left, const CMTime& right)
{
    return CMTimeAdd(left, right);
}

class reorder_buffer_t
{
public:

    struct entry_t
    {
        CFGuard<CVImageBufferRef> image;
        CMTime pts;
        CMTime duration;
        bool operator<(const entry_t& other) const
        {
            return pts < other.pts;
        }
    };

private:

    typedef boost::container::flat_set<entry_t> buffer_t;

public:

    reorder_buffer_t()
    {
    }

    void push(entry_t entry)
    {
        if (!_head)
            _head = entry.pts;
        _buffer.insert(std::move(entry));
    }

    bool empty() const
    {
        return _buffer.empty();
    }

    bool ready() const
    {
        return !empty() && _buffer.begin()->pts == _head;
    }

    entry_t pop()
    {
        assert(ready());
        auto entry = *_buffer.begin();
        _buffer.erase(_buffer.begin());
        _head = entry.pts + entry.duration;
        return entry;
    }

    void clear()
    {
        _buffer.clear();
        _head = boost::none;
    }

private:

    boost::optional<CMTime> _head;
    buffer_t _buffer;
};

1
这里有一个适用于任何所需缓冲区大小的解决方案,也不需要任何第三方库。我的C++代码可能不是最好的,但它可以工作。
我们创建一个Buffer结构体来通过pts标识缓冲区:
struct Buffer
{
    CVImageBufferRef imageBuffer = NULL;
    uint64_t pts = 0;
};

在我们的解码器中,我们需要跟踪缓冲区以及我们希望释放下一个的pts。
@property (nonatomic) std::vector <Buffer> buffers;
@property (nonatomic, assign) uint64_t nextExpectedPts;

现在我们已经准备好处理传入的缓冲区。在我的情况下,缓冲区是异步提供的。确保为解压会话提供正确的持续时间和呈现时间戳值,以便能够正确排序它们:
-(void)handleImageBuffer:(CVImageBufferRef)imageBuffer pts:(CMTime)presentationTimeStamp duration:(uint64_t)duration {
    //Situation 1, we can directly pass over this buffer
    if (self.nextExpectedPts == presentationTimeStamp.value || duration == 0) {
        [self sendImageBuffer:imageBuffer duration:duration];
        return;
    }
    //Situation 2, we got this buffer too fast. We will store it, but first we check if we have already stored the expected buffer
    Buffer futureBuffer = [self bufferWithImageBuffer:imageBuffer pts:presentationTimeStamp.value];
    int smallestPtsInBufferIndex = [self getSmallestPtsBufferIndex];
    if (smallestPtsInBufferIndex >= 0 && self.nextExpectedPts == self.buffers[smallestPtsInBufferIndex].pts) {
        //We found the next buffer, lets store the current buffer and return this one
        Buffer bufferWithSmallestPts = self.buffers[smallestPtsInBufferIndex];
        [self sendImageBuffer:bufferWithSmallestPts.imageBuffer duration:duration];
        CVBufferRelease(bufferWithSmallestPts.imageBuffer);
        [self setBuffer:futureBuffer atIndex:smallestPtsInBufferIndex];
    } else {
        //We dont have the next buffer yet, lets store this one to a new slot
        [self setBuffer:futureBuffer atIndex:self.buffers.size()];
    }
}

-(Buffer)bufferWithImageBuffer:(CVImageBufferRef)imageBuffer pts:(uint64_t)pts {
    Buffer futureBuffer = Buffer();
    futureBuffer.pts = pts;
    futureBuffer.imageBuffer = imageBuffer;
    CVBufferRetain(futureBuffer.imageBuffer);
    return futureBuffer;
}

- (void)sendImageBuffer:(CVImageBufferRef)imageBuffer duration:(uint64_t)duration {
    //Send your buffer to wherever you need it here
    self.nextExpectedPts += duration;
}

-(int) getSmallestPtsBufferIndex
{
    int minIndex = -1;
    uint64_t minPts = 0;
    for(int i=0;i<_buffers.size();i++) {
        if (_buffers[i].pts < minPts || minPts == 0) {
            minPts = _buffers[i].pts;
            minIndex = i;
        }
    }
    return minIndex;
}

- (void)setBuffer:(Buffer)buffer atIndex:(int)index {
    if (_buffers.size() <= index) {
        _buffers.push_back(buffer);
    } else {
        _buffers[index] = buffer;
    }
}

不要忘记在释放解码器时释放向量中的所有缓冲区,如果您正在使用循环文件,则需要跟踪文件何时完全循环以重置nextExpectedPts等内容。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接