Порядок отображения буфера изображения с VTDecompressionSession

У меня есть проект, в котором мне нужно декодировать видео h264 из живого сетевого потока и в конечном итоге получить текстуру, которую я могу отобразить в другом фреймворке (Unity3D) на устройствах iOS. Я могу успешно декодировать видео с помощью VTDecompressionSession, а затем получить текстуру с помощью CVMetalTextureCacheCreateTextureFromImage (или варианта OpenGL). Он отлично работает, когда я использую кодировщик с малой задержкой, и буферы изображений выводятся в порядке отображения, однако, когда я использую обычный кодировщик, буферы изображений не выводятся в порядке отображения, и изменение порядка буферов изображений, по-видимому, намного сложнее, чем Я ожидал.

Первая попытка состояла в том, чтобы установить VTDecodeFrameFlags с помощью kVTDecodeFrame_EnableAsynchronousDecompression и kVTDecodeFrame_EnableTemporalProcessing... Однако оказывается, что 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);
 }

При упорядочении метки времени на стороне обратного вызова монотонно увеличиваются с ожидаемой скоростью, но буферы располагаются не в правильном порядке. Кто-нибудь видит, где я делаю ошибку здесь? Или умеете определять порядок буферов на стороне обратного вызова? На данный момент я перепробовал практически все, о чем только мог подумать.


person Kaleb    schedule 20.10.2015    source источник
comment
ты решил это? Это делает мою голову.   -  person jsj    schedule 10.11.2015
comment
Я не решил это. Я уверен, что без переупорядочения видео, содержащее B-кадры, воспроизводится в порядке от высокого до среднего, как я и ожидал. Однако ясно, что видеокадры, хотя они и имеют порядок «высокий-нижний-средний», больше не связаны с презентационными метками «высокий-низкий-средний», которые поступают в обратном вызове. Это нарушает сортировку, и вы получаете странный порядок воспроизведения кадров. По крайней мере, я знаю, что я не один такой...   -  person Kaleb    schedule 10.11.2015
comment
Я просматривал реализацию XBMC, и у них есть комментарий в их обратном вызове, в котором говорится, что иногда кадры находятся в порядке декодирования, и очередь приоритетов, которую они используют для их изменения. Должен сказать, что меня не очень впечатлил API Video Toolbox, он плохо документирован, и эта ошибка довольно серьезная.   -  person jsj    schedule 11.11.2015
comment
Я предполагаю, что вы используете кодировку h264... Вы используете свой собственный кодировщик или кодировщик Apple? Я только что написал тестовое приложение, используя VTDecompressionSession с кодировщиком Apple, и оно отлично сортирует кадры. Похоже, VTDecompressionSession что-то не нравится в кодировщике, который я использую. :-/   -  person Kaleb    schedule 11.11.2015
comment
Думаю, это зависит от того, сколько у вас кадров B и P. У меня есть контент, созданный несколькими разными кодировщиками, и с некоторыми он кажется хуже, чем с другими, но да, я думаю, что это соотношение кадров B/P.   -  person jsj    schedule 12.11.2015
comment
Одна из проблем, которую я обнаружил, заключается в том, что неверные метки времени представления (PTS) связаны с моим кадром IDR и исходящими кадрами P и B. Это объясняет, почему переупорядочивание в обратном вызове не работает. Как ни странно, это не проблема для Android или других декодеров.   -  person Kaleb    schedule 12.11.2015


Ответы (3)


В моем случае проблема была не в VTDecompressionSession, а в том, что демультиплексор получил неправильный PTS. Хотя я не мог заставить VTDecompressionSession выводить кадры во временном (отображаемом) порядке с флагами kVTDecodeFrame_EnableAsynchronousDecompression и kVTDecodeFrame_EnableTemporalProcessing, я мог сам сортировать кадры на основе 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);

Что касается обратного вызова, нам нужен способ сортировки получаемых CVImageBufferRef. Я использую структуру, содержащую CVImageBufferRef и PTS. Затем вектор размером два, который будет выполнять фактическую сортировку.

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
         }
       }
 }
person Kaleb    schedule 17.11.2015

Я хотел бы немного улучшить этот ответ. Хотя описанное решение работает, оно требует знания количества кадров, необходимых для создания выходного кадра. В примере используется размер буфера 2, но в моем случае мне нужен размер буфера 3. Чтобы не указывать это заранее, можно использовать тот факт, что кадры (в порядке отображения) выравниваются exactly< /em> в терминах pts/duration. т.е. конец одного кадра точно является началом следующего. Таким образом, можно просто накапливать кадры до тех пор, пока в начале не будет «промежутка», затем вытолкнуть первый кадр и так далее. Также можно взять точки первого кадра (который всегда является I-кадром) в качестве начальной «головы» (поскольку она не обязательно должна быть нулевой...). Вот некоторый код, который делает это:

#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;
};
person maddanio    schedule 18.10.2017

Вот решение, которое работает с любым требуемым размером буфера, а также не требует сторонних библиотек. Мой код C++ может быть не самым лучшим, но он работает.

Мы создаем структуру Buffer для идентификации буферов по точкам:

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

В нашем декодере нам нужно отслеживать буферы и то, какие точки мы хотим освободить дальше:

@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 и тому подобное.

person Declan Stewart McPartlin    schedule 08.05.2020