整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

浏览器最新支持的WebCodecs 到底是什么?

浏览器最新支持的WebCodecs 到底是什么?

家好,很高兴又见面了,我是"高前端?进阶?",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

高前端?进阶

前言

现代技术提供了丰富的视频处理方式,比如 Media Stream API、Media Recording API、Media Source API 和 WebRTC API 共同组成了一个用于录制、传输和播放视频流的丰富工具集。

在Chrome 94+版本上已经支持了WebCodecs!

在解决某些高级任务时,这些 API 不允许 Web 开发者处理视频流的各个组成部分,例如帧、未混合的编码视频或音频块。 为了获得对这些基本组件的底层访问,开发人员一直在使用 WebAssembly 将视频和音频编解码器引入浏览器。 但鉴于现代浏览器已经附带了各种编解码器,将它们重新打包为 WebAssembly 似乎是对人力和计算机资源的浪费。

WebCodecs API 为开发者提供了一种使用浏览器中已经存在的媒体组件的方法,从而提高了效率。 具体包括以下部分:

  • 视频和音频解码器
  • 原始视频帧
  • 图像解码器

WebCodecs API 对于需要完全控制媒体内容处理方式的 Web 应用程序非常有用,例如视频编辑器、视频会议、视频流等。

1.视频处理工作流程

帧是视频处理的核心。 因此,在 WebCodecs 中,大多数类要么消费帧,要么生产帧。 视频编码器将帧转换为编码块,而视频解码器则相反。可以通过如下方法判断浏览器是否支持WebCodecs:

if (window.isSecureContext) {
  // 页面上下文完全,同时serviceWorker加载完成
  navigator.serviceWorker.register("/offline-worker.js").then(()=> {
  });
}
if ('VideoEncoder' in window) {
  // 支持WebCodecs API
}

请记住,WebCodecs 仅在安全上下文中可用,因此如果 self.isSecureContext 为 false,检测将失败!

VideoFrame 通过成为 CanvasImageSource 并具有接受 CanvasImageSource 的构造函数,可以很好地与其他 Web API 配合使用。 所以它可以用在 drawImage() 和 texImage2D() 等函数中。 它也可以由画布、位图、视频元素和其他视频帧构建。VideoFrame的构造函数如下:

new VideoFrame(image)
new VideoFrame(image, options)
new VideoFrame(data, options)
// 第二个参数为配置对象

下面是VideoFrame的一个典型示例:

const pixelSize=4;
const init={timestamp: 0, codedWidth: 320, codedHeight: 200, format: 'RGBA'};
let data=new Uint8Array(init.codedWidth * init.codedHeight * pixelSize);
for (let x=0; x < init.codedWidth; x++) {
  for (let y=0; y < init.codedHeight; y++) {
    let offset=(y * init.codedWidth + x) * pixelSize;
    data[offset]=0x7F;      
    // Red
    data[offset + 1]=0xFF;  
    // Green
    data[offset + 2]=0xD4; 
    // Blue
    data[offset + 3]=0x0FF; 
    // Alpha
  }
}
let frame=new VideoFrame(data, init);

WebCodecs API 与 Insertable Streams API 中的类协同工作,后者将 WebCodecs 连接到媒体流轨道(Media Stream Tracks)。

MediaStreamTrack 接口表示流中的单个媒体轨道;通常,这些是音频或视频轨道,但也可能存在其他轨道类型。

  • MediaStreamTrackProcessor 将媒体轨道分解为单独的帧。
  • MediaStreamTrackGenerator 从帧流创建媒体轨道。

2.WebCodecs 和Web Worker

根据设计,WebCodecs API 异步完成所有繁重的工作并脱离主线程。 但是由于框架和块回调通常可以每秒调用多次,它们可能会使主线程混乱,从而降低网站的响应速度。 因此,最好将单个帧和编码块的处理转移到Web Worker中。

为此,ReadableStream 提供了一种方便的方法来自动将来自媒体轨道的所有帧传输到工作程序。 例如,MediaStreamTrackProcessor 可用于获取来自网络摄像头的媒体流轨道的 ReadableStream。 之后,流被传输到Web Worker,其中帧被一个一个地读取并排队进入 VideoEncoder。

Streams API 的 ReadableStream 接口表示字节数据的可读流。 Fetch API 通过 Response 对象的 body 属性提供了 ReadableStream 的具体实例。

使用
HTMLCanvasElement.transferControlToOffscreen 甚至可以在主线程之外完成渲染。如果所有高级工具都不符合要求,VideoFrame 本身是可转移的,可以在Web Worker之间移动。

2.编码

这一切都始于 VideoFrame,可以通过三种方式构建视频帧。

  • 来自画布、图像位图或视频元素等图像源
const canvas=document.createElement("canvas");
// 在Canvas中绘制
const frameFromCanvas=new VideoFrame(canvas, { timestamp: 0 });
  • 使用 MediaStreamTrackProcessor 从 MediaStreamTrack拉取帧
const stream=await navigator.mediaDevices.getUserMedia({
  audio: true,
  video: {
    width: { min: 1024, ideal: 1280, max: 1920 },
    height: { min: 576, ideal: 720, max: 1080 }
  }
});
// 获取媒体帧的配置:https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
const track=stream.getTracks()[0];
const trackProcessor=new MediaStreamTrackProcessor(track);
const reader=trackProcessor.readable.getReader();
while (true) {
  const result=await reader.read();
  // 读取数据
  if (result.done) break;
  const frameFromCamera=result.value;
}
  • 从 BufferSource 中的二进制像素创建帧
const pixelSize=4;
const init={
  timestamp: 0,
  codedWidth: 320,
  codedHeight: 200,
  format: "RGBA",
};
const data=new Uint8Array(init.codedWidth * init.codedHeight * pixelSize);
// 创建Uint8Array对象
for (let x=0; x < init.codedWidth; x++) {
  for (let y=0; y < init.codedHeight; y++) {
    const offset=(y * init.codedWidth + x) * pixelSize;
    data[offset]=0x7f;      // Red
    data[offset + 1]=0xff;  // Green
    data[offset + 2]=0xd4;  // Blue
    data[offset + 3]=0x0ff; // Alpha
  }
}
const frame=new VideoFrame(data, init);
// 实例化VideoFrame对象

无论那种方式,都可以使用 VideoEncoder 将帧编码到 EncodedVideoChunk 对象中。在编码之前,需要给 VideoEncoder 两个 JavaScript 对象:

  • 带有两个函数的初始化对象,用于处理编码块和错误。 这些函数是开发人员定义的,在传递给 VideoEncoder 构造函数后无法更改。
  • 编码器配置对象,其中包含输出视频流的参数。 您可以稍后通过调用 configure() 来更改这些参数。

如果浏览器不支持配置,则 configure() 方法将抛出 NotSupportedError。 鼓励您使用配置调用静态方法
VideoEncoder.isConfigSupported() 以预先检查配置是否受支持并等待其promise的结果。

const init={
  output: handleChunk,
  // 处理快
  error: (e)=> {
    console.log(e.message);
  },
  // 处理错误
};
const config={
  codec: "vp8",
  width: 640,
  height: 480,
  bitrate: 2_000_000, // 2 Mbps
  framerate: 30,
};
const { supported }=await VideoEncoder.isConfigSupported(config);
// 判断是否支持
if (supported) {
  const encoder=new VideoEncoder(init);
  encoder.configure(config);
} else {
  // Try another config.
}

设置编码器后就可以通过 encode() 方法接受帧了。 configure() 和 encode() 都立即返回,无需等待实际工作完成。 它允许多个帧同时排队等待编码,而 encodeQueueSize 显示有多少请求在队列中等待先前的编码完成。

如果参数或方法调用顺序违反 API 约定,或者通过调用 error() 回调来解决编解码器实现中遇到的问题,可以通过立即抛出异常来报告错误。 如果编码成功完成,将使用新的编码块作为参数调用 output() 回调。

这里的另一个重要细节是,当不再需要框架时,需要通过调用 close() 来告知它们。

let frameCounter=0;
const track=stream.getVideoTracks()[0];
const trackProcessor=new MediaStreamTrackProcessor(track);
const reader=trackProcessor.readable.getReader();
while (true) {
  const result=await reader.read();
  if (result.done) break;
  const frame=result.value;
  if (encoder.encodeQueueSize > 2) {
    // 太多帧要处理,编码器过载,丢弃当前帧
    frame.close();
  } else {
    frameCounter++;
    const keyframe=frameCounter % 150==0;
    encoder.encode(frame, { keyFrame });
    frame.close();
  }
}

最后是通过编写一个函数来完成编码代码的时候了,该函数处理来自编码器的编码视频块。 通常此功能将通过网络发送数据块或将它们混合到媒体容器中进行存储。

function handleChunk(chunk, metadata) {
  if (metadata.decoderConfig) {
    // Decoder needs to be configured (or reconfigured) with new parameters
    // when metadata has a new decoderConfig.
    // Usually it happens in the beginning or when the encoder has a new
    // codec specific binary configuration. (VideoDecoderConfig.description).
    fetch("/upload_extra_data", {
      method: "POST",
      headers: { "Content-Type": "application/octet-stream" },
      body: metadata.decoderConfig.description,
    });
  }
  // 真实编码数据块大小
  const chunkData=new Uint8Array(chunk.byteLength);
  chunk.copyTo(chunkData);
  fetch(`/upload_chunk?timestamp=${chunk.timestamp}&type=${chunk.type}`, {
    method: "POST",
    headers: { "Content-Type": "application/octet-stream" },
    body: chunkData,
  });
}

如果在某个时候您需要确保所有待处理的编码请求都已完成,可以调用 flush() 并等待它的promise结果。

await encoder.flush();

3.解码

设置 VideoDecoder 与 VideoEncoder 类似:创建解码器时需要传递两个函数,并将编解码器参数提供给 configure()。

编解码器参数因编解码器而异。 例如,H.264 编解码器可能需要 AVCC 的二进制 blob,除非它以所谓的 Annex B 格式编码(encoderConfig.avc={ format: "annexb" })。

const init={
  output: handleFrame,
  error: (e)=> {
    console.log(e.message);
  },
};

const config={
  codec: "vp8",
  codedWidth: 640,
  codedHeight: 480,
};
const { supported }=await VideoDecoder.isConfigSupported(config);
if (supported) {
  const decoder=new VideoDecoder(init);
  // 实例化编码器
  decoder.configure(config);
  // 配置编码器
} else {
  // Try another config.
}

解码器初始化后,您可以开始为其提供 EncodedVideoChunk 对象。 要创建块,您需要:

  • 编码视频数据的 BufferSource
  • 以微秒为单位的块的开始时间戳(块中第一个编码帧的媒体时间)
  • 块的类型,其中之一:
  • key 如果块可以独立于以前的块解码
  • 增量,如果块只能在一个或多个先前的块被解码后被解码

此外,编码器发出的任何块都可以按原样为解码器准备好。 上面所说的关于错误报告和编码器方法的异步性质的所有内容对于解码器也同样适用。

const responses=await downloadVideoChunksFromServer(timestamp);
for (let i=0; i < responses.length; i++) {
  const chunk=new EncodedVideoChunk({
    timestamp: responses[i].timestamp,
    type: responses[i].key ? "key" : "delta",
    data: new Uint8Array(responses[i].body),
  });
  decoder.decode(chunk);
}
await decoder.flush();

现在是时候展示如何在页面上显示新解码的帧了。 最好确保解码器输出回调 (handleFrame()) 快速返回。 在下面的示例中,它仅将一个帧添加到准备渲染的帧队列中。 渲染是单独发生的,由两个步骤组成:

  • 等待合适的时间显示帧
  • 在画布上绘制帧

一旦不再需要某个帧,调用 close() 以在垃圾收集器到达它之前释放底层内存,这将减少 Web 应用程序使用的平均内存量。

const canvas=document.getElementById("canvas");
const ctx=canvas.getContext("2d");
let pendingFrames=[];
let underflow=true;
let baseTime=0;

function handleFrame(frame) {
  pendingFrames.push(frame);
  if (underflow) setTimeout(renderFrame, 0);
}

function calculateTimeUntilNextFrame(timestamp) {
  if (baseTime==0) baseTime=performance.now();
  let mediaTime=performance.now() - baseTime;
  return Math.max(0, timestamp / 1000 - mediaTime);
}

async function renderFrame() {
  underflow=pendingFrames.length==0;
  if (underflow) return;
  const frame=pendingFrames.shift();
  // Based on the frame's timestamp calculate how much of real time waiting
  // is needed before showing the next frame.
  const timeUntilNextFrame=calculateTimeUntilNextFrame(frame.timestamp);
  await new Promise((r)=> {
    setTimeout(r, timeUntilNextFrame);
  });
  ctx.drawImage(frame, 0, 0);
  frame.close();
  // 立即启动下一帧的调用逻辑
  setTimeout(renderFrame, 0);
}

参考资料


https://developer.chrome.com/articles/webcodecs/(Chrome官方文档)

https://developer.mozilla.org/en-US/docs/Web/API/isSecureContext

https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts

https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream

https://www.w3.org/2020/06/machine-learning-workshop/talks/media_processing_hooks_for_the_web.html

将编码的视频流解码为原始视频数据,编码视频流可以来自网络流或文件,解码后即可渲染到屏幕.

实现原理

正如我们所知,编码数据仅用于传输,无法直接渲染到屏幕上,所以这里利用FFmpeg解析文件中的编码的视频流,并将压缩视频数据(h264/h265)解码为指定格式(yuv,RGB)的视频原始数据,以渲染到屏幕上。 注意: 本例主要为解码,需要借助FFmpeg搭建模块,视频解析模块,渲染模块,这些模块在下面阅读前提皆有链接可直接访问。

总体架构 简易流程

FFmpeg parse流程

  • 创建format context: avformat_alloc_context
  • 打开文件流: avformat_open_input
  • 寻找流信息: avformat_find_stream_info
  • 获取音视频流的索引值: formatContext->streams[i]->codecpar->codec_type==(isVideoStream ? AVMEDIA_TYPE_VIDEO : AVMEDIA_TYPE_AUDIO)
  • 获取音视频流: m_formatContext->streams[m_audioStreamIndex]
  • 解析音视频数据帧: av_read_frame
  • 获取extra data: av_bitstream_filter_filter

FFmpeg decode流程

  • 确定解码器类型: enum AVHWDeviceType av_hwdevice_find_type_by_name(const char *name)
  • 创建视频流: int av_find_best_stream(AVFormatContext *ic,enum FfmpegaVMediaType type,int wanted_stream_nb,int related_stream,AVCodec **decoder_ret,int flags);
  • 初始化解码器: AVCodecContext *avcodec_alloc_context3(const AVCodec *codec)
  • 填充解码器上下文: int avcodec_parameters_to_context(AVCodecContext *codec, const AVCodecParameters *par);
  • 打开指定类型的设备: int av_hwdevice_ctx_create(AVBufferRef **device_ctx, enum AVHWDeviceType type, const char *device, AVDictionary *opts, int flags)
  • 初始化编码器上下文对象: int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options)
  • 初始化视频帧: AVFrame *av_frame_alloc(void)
  • 找到第一个I帧开始解码: packet.flags==1
  • 将parse到的压缩数据送给解码器: int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt)
  • 接收解码后的数据: int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame)
  • 构造时间戳
  • 将解码后的数据存到CVPixelBufferRef并将其转为CMSampleBufferRef,解码完成

文件结构

快速使用

  • 初始化preview
- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupUI];
}

- (void)setupUI {
    self.previewView=[[XDXPreviewView alloc] initWithFrame:self.view.frame];
    [self.view addSubview:self.previewView];
    [self.view bringSubviewToFront:self.startBtn];
}

解析并解码文件中视频数据

- (void)startDecodeByFFmpegWithIsH265Data:(BOOL)isH265 {
    NSString *path=[[NSBundle mainBundle] pathForResource:isH265 ? @"testh265" : @"testh264" ofType:@"MOV"];
    XDXAVParseHandler *parseHandler=[[XDXAVParseHandler alloc] initWithPath:path];
    XDXFFmpegVideoDecoder *decoder=[[XDXFFmpegVideoDecoder alloc] initWithFormatContext:[parseHandler getFormatContext] videoStreamIndex:[parseHandler getVideoStreamIndex]];
    decoder.delegate=self;
    [parseHandler startParseGetAVPackeWithCompletionHandler:^(BOOL isVideoFrame, BOOL isFinish, AVPacket packet) {
        if (isFinish) {
            [decoder stopDecoder];
            return;
        }
        
        if (isVideoFrame) {
            [decoder startDecodeVideoDataWithAVPacket:packet];
        }
    }];
}

将解码后数据渲染到屏幕上

-(void)getDecodeVideoDataByFFmpeg:(CMSampleBufferRef)sampleBuffer {
    CVPixelBufferRef pix=CMSampleBufferGetImageBuffer(sampleBuffer);
    [self.previewView displayPixelBuffer:pix];
}

C++音视频开发学习资料:点击领取→音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)

具体实现

1. 初始化实例对象

因为本例中的视频数据源是文件,而format context上下文实在parse模块初始化的,所以这里仅仅需要将其传入解码器即可.

- (instancetype)initWithFormatContext:(AVFormatContext *)formatContext videoStreamIndex:(int)videoStreamIndex {
    if (self=[super init]) {
        m_formatContext=formatContext;
        m_videoStreamIndex=videoStreamIndex;
        
        m_isFindIDR=NO;
        m_base_time=0;
        
        [self initDecoder];
    }
    return self;
}

2. 初始化解码器

- (void)initDecoder {
    // 获取视频流
    AVStream *videoStream=m_formatContext->streams[m_videoStreamIndex];
    // 创建解码器上下文对象
    m_videoCodecContext=[self createVideoEncderWithFormatContext:m_formatContext
                                                            stream:videoStream
                                                  videoStreamIndex:m_videoStreamIndex];
    if (!m_videoCodecContext) {
        log4cplus_error(kModuleName, "%s: create video codec failed",__func__);
        return;
    }
    
    // 创建视频帧
    m_videoFrame=av_frame_alloc();
    if (!m_videoFrame) {
        log4cplus_error(kModuleName, "%s: alloc video frame failed",__func__);
        avcodec_close(m_videoCodecContext);
    }
}

2.1. 创建解码器上下文对象

- (AVCodecContext *)createVideoEncderWithFormatContext:(AVFormatContext *)formatContext stream:(AVStream *)stream videoStreamIndex:(int)videoStreamIndex {
    AVCodecContext *codecContext=NULL;
    AVCodec *codec=NULL;
    
    // 指定解码器名称, 这里使用苹果VideoToolbox中的硬件解码器
    const char *codecName=av_hwdevice_get_type_name(AV_HWDEVICE_TYPE_VIDEOTOOLBOX);
    // 将解码器名称转为对应的枚举类型
    enum AVHWDeviceType type=av_hwdevice_find_type_by_name(codecName);
    if (type !=AV_HWDEVICE_TYPE_VIDEOTOOLBOX) {
        log4cplus_error(kModuleName, "%s: Not find hardware codec.",__func__);
        return NULL;
    }
    
    // 根据解码器枚举类型找到解码器
    int ret=av_find_best_stream(formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
    if (ret < 0) {
        log4cplus_error(kModuleName, "av_find_best_stream faliture");
        return NULL;
    }
    
    // 为解码器上下文对象分配内存
    codecContext=avcodec_alloc_context3(codec);
    if (!codecContext){
        log4cplus_error(kModuleName, "avcodec_alloc_context3 faliture");
        return NULL;
    }
    
    // 将视频流中的参数填充到视频解码器中
    ret=avcodec_parameters_to_context(codecContext, formatContext->streams[videoStreamIndex]->codecpar);
    if (ret < 0){
        log4cplus_error(kModuleName, "avcodec_parameters_to_context faliture");
        return NULL;
    }
    
    // 创建硬件解码器上下文
    ret=InitHardwareDecoder(codecContext, type);
    if (ret < 0){
        log4cplus_error(kModuleName, "hw_decoder_init faliture");
        return NULL;
    }
    
    // 初始化解码器上下文对象
    ret=avcodec_open2(codecContext, codec, NULL);
    if (ret < 0) {
        log4cplus_error(kModuleName, "avcodec_open2 faliture");
        return NULL;
    }
    
    return codecContext;
}

#pragma mark - C Function
AVBufferRef *hw_device_ctx=NULL;
static int InitHardwareDecoder(AVCodecContext *ctx, const enum AVHWDeviceType type) {
    int err=av_hwdevice_ctx_create(&hw_device_ctx, type, NULL, NULL, 0);
    if (err < 0) {
        log4cplus_error("XDXParseParse", "Failed to create specified HW device.\n");
        return err;
    }
    ctx->hw_device_ctx=av_buffer_ref(hw_device_ctx);
    return err;
}

av_find_best_stream : 在文件中找到最佳流信息.

  • ic: 媒体文件
  • type: video, audio, subtitles...
  • wanted_stream_nb: 用户请求的流编号,-1表示自动选择
  • related_stream: 试着找到一个相关的流,如果没有可填-1
  • decoder_ret: 非空返回解码器引用
  • flags: 保留字段
  • avcodec_parameters_to_context: 根据提供的解码器参数中的值填充解码器上下文

仅仅将解码器中具有相应字段的任何已分配字段par被释放并替换为par中相应字段的副本。不涉及解码器中没有par中对应项的字段。

  • av_hwdevice_ctx_create: 打开指定类型的设备并为其创建AVHWDeviceContext。
  • avcodec_open2: 使用给定的AVCodec初始化AVCodecContext,在使用此函数之前,必须使用avcodec_alloc_context3()分配内存。
int av_find_best_stream(AVFormatContext *ic,
                        enum FfmpegaVMediaType type,
                        int wanted_stream_nb,
                        int related_stream,
                        AVCodec **decoder_ret,
                        int flags);

2.2. 创建视频帧 AVFrame

作为解码后原始的音视频数据的容器.AVFrame通常被分配一次然后多次重复(例如,单个AVFrame以保持从解码器接收的帧)。在这种情况下,av_frame_unref()将释放框架所持有的任何引用,并在再次重用之前将其重置为其原始的清理状态。

    // Get video frame
    m_videoFrame=av_frame_alloc();
    if (!m_videoFrame) {
        log4cplus_error(kModuleName, "%s: alloc video frame failed",__func__);
        avcodec_close(m_videoCodecContext);
    }

3. 开始解码

首先找到编码数据流中第一个I帧, 然后调用avcodec_send_packet将压缩数据发送给解码器.最后利用循环接收avcodec_receive_frame解码后的视频数据.构造时间戳,并将解码后的数据填充到CVPixelBufferRef中并将其转为CMSampleBufferRef.

- (void)startDecodeVideoDataWithAVPacket:(AVPacket)packet {
    if (packet.flags==1 && m_isFindIDR==NO) {
        m_isFindIDR=YES;
        m_base_time=m_videoFrame->pts;
    }
    
    if (m_isFindIDR==YES) {
        [self startDecodeVideoDataWithAVPacket:packet
                             videoCodecContext:m_videoCodecContext
                                    videoFrame:m_videoFrame
                                      baseTime:m_base_time
                              videoStreamIndex:m_videoStreamIndex];
    }
}

- (void)startDecodeVideoDataWithAVPacket:(AVPacket)packet videoCodecContext:(AVCodecContext *)videoCodecContext videoFrame:(AVFrame *)videoFrame baseTime:(int64_t)baseTime videoStreamIndex:(int)videoStreamIndex {
    Float64 current_timestamp=[self getCurrentTimestamp];
    AVStream *videoStream=m_formatContext->streams[videoStreamIndex];
    int fps=DecodeGetAVStreamFPSTimeBase(videoStream);
    
    
    avcodec_send_packet(videoCodecContext, &packet);
    while (0==avcodec_receive_frame(videoCodecContext, videoFrame))
    {
        CVPixelBufferRef pixelBuffer=(CVPixelBufferRef)videoFrame->data[3];
        CMTime presentationTimeStamp=kCMTimeInvalid;
        int64_t originPTS=videoFrame->pts;
        int64_t newPTS=originPTS - baseTime;
        presentationTimeStamp=CMTimeMakeWithSeconds(current_timestamp + newPTS * av_q2d(videoStream->time_base) , fps);
        CMSampleBufferRef sampleBufferRef=[self convertCVImageBufferRefToCMSampleBufferRef:(CVPixelBufferRef)pixelBuffer
                                                                   withPresentationTimeStamp:presentationTimeStamp];
        
        if (sampleBufferRef) {
            if ([self.delegate respondsToSelector:@selector(getDecodeVideoDataByFFmpeg:)]) {
                [self.delegate getDecodeVideoDataByFFmpeg:sampleBufferRef];
            }
            
            CFRelease(sampleBufferRef);
        }
    }
}
  • avcodec_send_packet: 将压缩视频帧数据送给解码器
    • AVERROR(EAGAIN): 当前状态下不接受输入,用户必须通过avcodec_receive_frame()读取输出的buffer. (一旦所有输出读取完毕,packet应该被重新发送,调用不会失败)
    • AVERROR_EOF: 解码器已经被刷新,没有新的packet能发送给它.
    • AVERROR(EINVAL): 解码器没有被打开
    • AVERROR(ENOMEM): 将Packet添加到内部队列失败.
  • avcodec_receive_frame: 从解码器中获取解码后的数据
    • AVERROR(EAGAIN): 输出不可用, 用户必须尝试发送一个新的输入数据
    • AVERROR_EOF: 解码器被完全刷新,这儿没有更多的输出帧
    • AVERROR(EINVAL): 解码器没有被打开.
    • 其他负数: 解码错误.

    4. 停止解码

    释放相关资源



    因为公司项目需要,研究了下腾讯的实时音视频的C# Winform版本的Demo。

    当然其他平台也有,不是我主要负责。

    经过2天的摸索,对其代码和原理进行了一个简单的梳理。因为才接触腾讯的音视频直播,同时C# Winform相关的知识已经5年没碰了。

    所以下面的内容,应该会出现一些偏差,仅供大家参考。

    腾讯的Demo 下载地址:

    核心类解读

    其实整个项目的最为核心的文件是:TXLiteAVVideoViews。翻译过来的意思应该是腾讯轻量级的音视频视图。

    在这个文件中,包含了3个非常重要的类,我认为音视频在C#`层面上的核心。

    • TXLiteAVVideoView,继承自Pannel,将视频帧渲染到空间上。
    • TXLiteAVVideoViewManager,继承自ITRTCVideoRenderCallBack,承上启下,将底层传递的帧数据再传递给TXLiteAVVideoView
    • FrameBufferInfo帧数据的封装实体。

    TXLiteAVVideoView

    以下为我把非核心代码删除后的简单模型。

    public class TXLiteAVVideoView: Panel {
    
    	// 帧缓存,可以理解为视频暂停时候的一个画面
    	private volatile FrameBufferInfo _mArgbFrame=new FrameBufferInfo();
    
        public bool AppendVideoFrame(byte[] data, int width, int height, TRTCVideoPixelFormat videoFormat, TRTCVideoRotation rotation) {
    		//...
    	}
        
    	protected override void OnPaint(PaintEventArgs pe) {
    		//....
    	}
    }
    
    • 该类继承了Pannl,又重写了OnPaint,所以可以猜测目的是为了根据Frame数据来绘图。
    • _mArgbFrame 的作用是保存的某一时刻的一帧数据,保存起来的目的是为了方便OnPaint来绘图,它由什么地方传递过来的了,我们看下面这段话?
    • AppendVideoFrame由TXLiteAVVideoViewManager来调用,其中就传入了byte[] data这个还没有处理的帧的数据。

    所以由此我们可以简单分析总结下:该类通过方法AppendVideoFrame接收TXLiteAVVideoViewManager传递过来的帧数据,在将帧数据保存到局部变量_mArgbFrame后调用refresh方法,该方法会调用重写后的OnPaint来画图。

    TXLiteAVVideoViewManager

    同样简化下代码:

    class TXLiteAVVideoViewManager: ITRTCVideoRenderCallback {
        
    	private volatile Dictionary<string,TXLiteAVVideoView> _mMapViews;
    
    	public void onRenderVideoFrame(string userId, TRTCVideoStreamType streamType, TRTCVideoFrame frame) {
            //...
        }
    }
    
    • 该类实现了接口ITRTCVideoRenderCallback的方法onRenderVideoFrame。通过签名,我大胆的猜测了从服务器拉数据的时候,数据中应该有远程用户的id,以及对应的数据帧和类型。
    • 其底层可能在不停的拉数据,然后不停的调用这个实现类来传递给对应的TXLiteAVVideoView进行视图渲染。
    • _mMapViews这个局部变量,通过userId-streamType来作为key,其TXLiteAVVideView作为value来保存的数据。

    我们可以简单看看onRenderVideoFrame的实现

    public void onRenderVideoFrame(string userId, TRTCVideoStreamType streamType, TRTCVideoFrame frame) {
    	//....
    	TXLiteAVVideoView view=null;
    	lock(_mMapViews) {
    		view=_mMapViews[GetKey(userId, streamType)];
    	}
    	//调用 AppendVideoFrame 进行帧的渲染
    	view?.AppendVideoFrame(frame.data, (int) frame.width, (int) frame.height, frame.videoFormat, frame.rotation);
    }
    

    其本质也是从Dictionary中通过GetKey(userId, streamType)来构成key,获取到对应的view,然后进行AppendVideoFrame.

    FrameBufferInfo

    这个类的实现如下:

    class FrameBufferInfo
    {
            public byte[] data { get; set; }
    
            public int width { get; set; }
    
            public int height { get; set; }
    
            public bool newFrame { get; set; }
    
            /**
             * Rotation 是是否旋转
             */
            public TRTCVideoRotation rotation { get; set; }
    }
    

    表示的应该是这个帧如何处理。

    结语

    这是腾讯音视频实时通信的第一篇分析,后面会根据情况,看看有没有更多有意义的可以写文。

    希望对大家有帮助。


    如果本文对你有帮助,欢迎评论、点赞、转发、收藏、关注!