MJPEG 简介
Motion JPEG(M-JPEG或MJPEG,Motion Joint Photographic Experts Group,FourCC:MJPG)是一种影像压缩格式,其中每一帧图像都分别使用JPEG编码。M-JPEG常用在数码相机和摄像头之类的图像采集设备上,非线性剪辑系统也常用这种格式。QuickTime播放器和包括Mozilla Firefox,Google Chrome,Safari在内许多网页浏览器原生支持M-JPEG。
—— https://zh.m.wikipedia.org/zh-hans/Motion_JPEG
对于网络摄像头时,使用 MJPEG 是一个比较低成本的方案,并且非常时候局域网配置。因为不需要很高的压缩效率,替换 H264、H265 会省下专利费和芯片成本。
MJPEG 流没有统一的规范,微软使用很老的 AVI 格式封装,Mac 平台则用 Mp4 封装。对于流式传输,则是各个摄像头厂商自己定义协议了,所以需要实自己现播放。下面介绍各个播放框架中,怎么实现 MJPEG 的播放。
DirectShow 框架
实现一个 SourceFilter,输出 MJPEG 格式的压缩视频。
关键的配置代码如下:
static HRESULT FillMJPG(CMediaType* pMediaType, Bambu_StreamInfo const* m_info)
{
VIDEOINFOHEADER* pvi =
(VIDEOINFOHEADER*)pMediaType->AllocFormatBuffer(sizeof(VIDEOINFOHEADER));
if (pvi == 0)
return(E_OUTOFMEMORY);
ZeroMemory(pvi, pMediaType->cbFormat);
int sizeImage = m_info->format.video.width * m_info->format.video.height * 3;
pMediaType->SetSubtype(&MEDIASUBTYPE_MJPG);
pMediaType->SetFormatType(&FORMAT_VideoInfo);
pMediaType->SetTemporalCompression(FALSE);
pMediaType->SetSampleSize(sizeImage);
SetRect(&(pvi->rcSource), 0, 0, m_info->format.video.width, m_info->format.video.height);
CopyRect(&(pvi->rcTarget), &(pvi->rcSource));
pvi->AvgTimePerFrame = 0;//UNITS / m_info->video_format.frame_rate;
BITMAPINFOHEADER* bmi = &pvi->bmiHeader;
bmi->biSize = sizeof(BITMAPINFOHEADER);
bmi->biPlanes = 1;
bmi->biClrImportant = 0;
bmi->biClrUsed = 0;
bmi->biWidth = m_info->format.video.width;
bmi->biHeight = m_info->format.video.height;
bmi->biSizeImage = sizeImage;
bmi->biBitCount = 24;
bmi->biCompression = 'GPJM'; // MJPG
return S_OK;
}
其中 biBitCount 一定要设置,因为 jpg 没有 alpha 通道,所以一般是 24 bit 深度。我一开始没有设置,与 “MJPEG Decompressor” 就无法连接。
VideoToolBox 解码
在 Mac 平台,使用 VideoToolBox 解码 MJPG 格式的视频。
关键的配置代码如下:
status = CMVideoFormatDescriptionCreate(kCFAllocatorDefault, kCMVideoCodecType_JPEG,
stream_info.format.video.width, stream_info.format.video.height,
NULL, &formatDescription);
另外发现一个诡异的问题:图像显示顺序是乱的,会显示一张新图像,再显示一张之前的图像,如此反复。
后来发现,在解码回调函数里面拿到 IOSurfaceRef 图像,必须马上使用,post 到 ui 线程处理时,要等待 ui 线程处理完成。但是同样的代码,对于 H264 解码器,是正常的。
改成同步等待(用 dispatch_sync 代替 dispatch_async),就 OK 了。
- (void) didDecompress: (OSStatus) status with:(CVImageBufferRef) imageBuffer {
IOSurfaceRef surface = CVPixelBufferGetIOSurface(imageBuffer);
dispatch_sync(dispatch_get_main_queue(), ^{
if (self->playing && surface != nil)
view.layer.contents = (__bridge id) surface;
});
}