Мне удалось создать приложение, которое получает живой видеопоток в формате h264, а затем декодирует и отображает видео с помощью Video Toolbox и AVSampleBufferDisplayLayer. Это работает, как и ожидалось, но я хочу иметь возможность применять фильтры к отображаемому выводу, поэтому я переключился на декодирование с помощью Video Toolbox и отображение / рендеринг декодированного видео с помощью MetalKit. Единственная проблема, с которой я столкнулся, заключается в том, что мои выходные данные с помощью MetalKit заметно более размытые, чем выходные данные, полученные с помощью AVSampleBufferDisplayLayer, и мне не удалось выяснить, почему.
Вот вывод из AVSampleBufferDisplayLayer
Вот вывод из MetalKit
Я попытался пропустить MetalKit и выполнить рендеринг непосредственно в CAMetalLayer, но проблема сохраняется. Я пытаюсь преобразовать свой CVImageBufferRef в UIImage, который я могу отобразить с помощью UIView. Если это также в конечном итоге расплывчато, то, возможно, проблема связана с моей VTDecompressionSession, а не с металлической стороной вещей.
Декодирующая часть очень похожа на приведенную здесь Как использовать VideoToolbox для распаковки видеопотока H.264
Я постараюсь просто вставить интересные фрагменты моего кода.
Это параметры, которые я даю моей VTDecompressionSession.
NSDictionary *destinationImageBufferAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInteger:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange],
(id)kCVPixelBufferPixelFormatTypeKey,
nil];
Это мой взгляд, который наследуется от MTKView
@interface StreamView : MTKView
@property id<MTLCommandQueue> commandQueue;
@property id<MTLBuffer> vertexBuffer;
@property id<MTLBuffer> colorConversionBuffer;
@property id<MTLRenderPipelineState> pipeline;
@property CVMetalTextureCacheRef textureCache;
@property CFMutableArrayRef imageBuffers;
-(id)initWithRect:(CGRect)rect withDelay:(int)delayInFrames;
-(void)addToRenderQueue:(CVPixelBufferRef)image renderAt:(int)frame;
@end
Вот как я инициализирую представление из моего контроллера представления. Видео, которое я получаю, имеет тот же размер, то есть 666x374.
streamView = [[StreamView alloc] initWithRect:CGRectMake(0, 0, 666, 374) withDelay:0];
[self.view addSubview:streamView];
Это содержимое метода initWithRect в StreamView
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
self = [super initWithFrame:rect device:device];
self.colorPixelFormat = MTLPixelFormatBGRA8Unorm;
self.commandQueue = [self.device newCommandQueue];
[self buildTextureCache];
[self buildPipeline];
[self buildVertexBuffers];
Это метод buildPipeline
- (void)buildPipeline
{
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
id<MTLLibrary> library = [self.device newDefaultLibraryWithBundle:bundle error:NULL];
id<MTLFunction> vertexFunc = [library newFunctionWithName:@"vertex_main"];
id<MTLFunction> fragmentFunc = [library newFunctionWithName:@"fragment_main"];
MTLRenderPipelineDescriptor *pipelineDescriptor = [MTLRenderPipelineDescriptor new];
pipelineDescriptor.vertexFunction = vertexFunc;
pipelineDescriptor.fragmentFunction = fragmentFunc;
pipelineDescriptor.colorAttachments[0].pixelFormat = self.colorPixelFormat;
self.pipeline = [self.device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:NULL];
}
Вот как я на самом деле рисую свою текстуру
CVImageBufferRef image = (CVImageBufferRef)CFArrayGetValueAtIndex(_imageBuffers, 0);
id<MTLTexture> textureY = [self getTexture:image pixelFormat:MTLPixelFormatR8Unorm planeIndex:0];
id<MTLTexture> textureCbCr = [self getTexture:image pixelFormat:MTLPixelFormatRG8Unorm planeIndex:1];
if(textureY == NULL || textureCbCr == NULL)
return;
id<CAMetalDrawable> drawable = self.currentDrawable;
id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
MTLRenderPassDescriptor *renderPass = self.currentRenderPassDescriptor;
renderPass.colorAttachments[0].clearColor = MTLClearColorMake(0.5, 1, 0.5, 1);
id<MTLRenderCommandEncoder> commandEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPass];
[commandEncoder setRenderPipelineState:self.pipeline];
[commandEncoder setVertexBuffer:self.vertexBuffer offset:0 atIndex:0];
[commandEncoder setFragmentTexture:textureY atIndex:0];
[commandEncoder setFragmentTexture:textureCbCr atIndex:1];
[commandEncoder setFragmentBuffer:_colorConversionBuffer offset:0 atIndex:0];
[commandEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4 instanceCount:1];
[commandEncoder endEncoding];
[commandBuffer presentDrawable:drawable];
[commandBuffer commit];
Вот так я конвертирую CVPixelBufferRef в текстуру MTL
- (id<MTLTexture>)getTexture:(CVPixelBufferRef)image pixelFormat:(MTLPixelFormat)pixelFormat planeIndex:(int)planeIndex {
id<MTLTexture> texture;
size_t width, height;
if (planeIndex == -1)
{
width = CVPixelBufferGetWidth(image);
height = CVPixelBufferGetHeight(image);
planeIndex = 0;
}
else
{
width = CVPixelBufferGetWidthOfPlane(image, planeIndex);
height = CVPixelBufferGetHeightOfPlane(image, planeIndex);
NSLog(@"texture %d, %ld, %ld", planeIndex, width, height);
}
CVMetalTextureRef textureRef = NULL;
CVReturn status = CVMetalTextureCacheCreateTextureFromImage(NULL, _textureCache, image, NULL, pixelFormat, width, height, planeIndex, &textureRef);
if(status == kCVReturnSuccess)
{
texture = CVMetalTextureGetTexture(textureRef);
CFRelease(textureRef);
}
else
{
NSLog(@"CVMetalTextureCacheCreateTextureFromImage failed with return stats %d", status);
return NULL;
}
return texture;
}
Это мой фрагментный шейдер
fragment float4 fragment_main(Varyings in [[ stage_in ]],
texture2d<float, access::sample> textureY [[ texture(0) ]],
texture2d<float, access::sample> textureCbCr [[ texture(1) ]],
constant ColorConversion &colorConversion [[ buffer(0) ]])
{
constexpr sampler s(address::clamp_to_edge, filter::linear);
float3 ycbcr = float3(textureY.sample(s, in.texcoord).r, textureCbCr.sample(s, in.texcoord).rg);
float3 rgb = colorConversion.matrix * (ycbcr + colorConversion.offset);
return float4(rgb, 1.0);
}
Поскольку и код, и код, который я кодирую, имеют размер 666x374, я попытался изменить тип выборки в фрагментном шейдере на filter :: near. Я думал, что это будет соответствовать пикселям 1: 1, но все равно было размыто. Еще одна странная вещь, которую я заметил, заключается в том, что если вы откроете загруженные изображения в новой вкладке, вы увидите, что они намного больше, чем 666x374 ... Я сомневаюсь, что я делаю ошибку на стороне кодирования, и даже если я это сделал AVSampleBufferDisplayLayer по-прежнему удается отображать видео без размытия, поэтому они должны делать что-то правильно, что мне не хватает.