第五十章 视频编码后修改帧数据

基本概念

给视频添加滤镜的方法,是在视频帧被编码前对其进行处理。有时我们还需要在视频帧被编码后对其进行处理,插入一些自定义数据,比如插入 H.264 定义的 SEI(Supplemental Enhancement Information,补充增强信息)。

此时便可以使用 WebRTC Insertable Streams API,这组 API 在 WebRTC M83 (branch-heads/4103) 版本被引入,会分别在视频帧被编码后且发送前、或者被接收后且解码前被调用。由于笔者是 Native 开发,因此这里直接给出 C++ 代码;前端开发者则可以直接参照 W3C 对应文档

源代码解析

首先我们需要继承并实现 webrtc::FrameTransformerInterface ,一个典型模版如下:

ExampleTransformer.cpp

namespace example {

// 在头文件中定义
// class ExampleTransformer : public webrtc::FrameTransformerInterface {
//  public:
//   virtual void RegisterTransformedFrameSinkCallback(
//       rtc::scoped_refptr<webrtc::TransformedFrameCallback>, uint32_t ssrc) override;
//   virtual void UnregisterTransformedFrameSinkCallback(uint32_t ssrc) override;
//   virtual void Transform(std::unique_ptr<webrtc::TransformableFrameInterface> frame) override;
//  private:
//   mutable webrtc::Mutex mutex_;
//   rtc::scoped_refptr<webrtc::TransformedFrameCallback> sink_callback_;
// }

  // WebRTC 只会在 webrtc::RTPSenderVideo 的构造方法中注册一个 callback,
  // 具体可以参见 rtp_sender_video.cc frame_transformer_delegate_->Init()
  void ExampleTransformer::RegisterTransformedFrameSinkCallback(
      rtc::scoped_refptr<webrtc::TransformedFrameCallback> callback, uint32_t ssrc) {
    webrtc::MutexLock lock(&mutex_);
    sink_callback_ = callback;
  }

  void ExampleTransformer::UnregisterTransformedFrameSinkCallback(uint32_t ssrc) {
    webrtc::MutexLock lock(&mutex_);
    sink_callback_ = nullptr;
  }

  void ExampleTransformer::Transform(std::unique_ptr<webrtc::TransformableFrameInterface> frame) {
    webrtc::MutexLock lock(&mutex_);
    if (sink_callback_ == nullptr) return;

    // 在这里处理 frame 的二进制数据...
    // 处理完毕后务必调用 sink_callback_ 将帧数据传递给上层
    sink_callback_->OnTransformedFrame(std::move(frame));
  }
}

如果你想要在视频帧被编码后且发送前被调用,可以通过 RtpSender 进行设置:

rtp_sender_interface.h

class RTC_EXPORT RtpSenderInterface : public rtc::RefCountInterface {
 public:
  // other definitions...

  // 设置的 frame_transformer 会在视频帧被编码后且发送前调用
  virtual void SetEncoderToPacketizerFrameTransformer(
      rtc::scoped_refptr<FrameTransformerInterface> frame_transformer);

  // other definitions...
}

如果你想要在视频帧被接收后且解码前被调用,可以通过 RtpReceiver 进行设置:

rtp_receiver_interface.h

class RTC_EXPORT RtpReceiverInterface : public rtc::RefCountInterface {
 public:
  // other definitions...

  // 设置的 frame_transformer 会在视频帧被接收后且解码前被调用
  virtual void SetDepacketizerToDecoderFrameTransformer(
      rtc::scoped_refptr<FrameTransformerInterface> frame_transformer);

  // other definitions...
}

最后,这组 API 也同样适用于音频,因此读者需要注意 transform 过程中 frame 的具体类型。如果是视频帧数据,实际类型为 webrtc::TransformableVideoFrameInterface ;如果是音频帧数据,则实际类型为 webrtc::TransformableAudioFrameInterface

在我们的流式中真实的源代码如下:

这是使用的入口,在Addtrack之后

//设置可插入流处理类
                                auto rtcSender = result_or_error.value();
                                if (rtcSender)
                                {
                                        rtc::scoped_refptr<StreamDataTransformer> myTransformer(new rtc::RefCountedObject<StreamDataTransformer>("TestName"));
                                        rtcSender->SetEncoderToPacketizerFrameTransformer(myTransformer);
                                }
                                else
                                {
                                        QL_ERROR("Invalid rtcSender");
                                }

真实处理逻辑的类封装中的头文件

#pragma once
#include "api/frame_transformer_interface.h"

namespace QL
{
                        class StreamDataTransformer : public webrtc::FrameTransformerInterface
                        {
                        public:
                                StreamDataTransformer(std::string trackId);
                                // Transforms |frame| using the implementing class' processing logic.
                                virtual void Transform(
                                        std::unique_ptr<webrtc::TransformableFrameInterface> transformable_frame);
                                virtual void RegisterTransformedFrameCallback(rtc::scoped_refptr<webrtc::TransformedFrameCallback> callback);
                                virtual void RegisterTransformedFrameSinkCallback(rtc::scoped_refptr<webrtc::TransformedFrameCallback> callback, uint32_t ssrc);
                                virtual void UnregisterTransformedFrameCallback();
                                virtual void UnregisterTransformedFrameSinkCallback(uint32_t ssrc);

                                //virtual void AddRef() const override;
                                //virtual rtc::RefCountReleaseStatus Release() const override;

                        private:
                                std::string mCellName;
                                rtc::scoped_refptr<webrtc::TransformedFrameCallback> sink_callback_;
                        };

}

Cpp文件

#include "StreamDataTransformer.h"

namespace QL
{
        StreamDataTransformer::StreamDataTransformer(std::string cellName)
                        {
                                this->mCellName = cellName;
                        }

                        /// <summary>
                        /// 
                        /// </summary>
                        /// <param name="transformable_frame"></param>
                        void StreamDataTransformer::Transform(std::unique_ptr<webrtc::TransformableFrameInterface> transformable_frame)
                        {
                                const int ourAdditionalDataLength = 32;

                                auto frameData = transformable_frame->GetData();
                                size_t dataSize = frameData.size();
                                //新的帧内容长度=原来的长+新增数据的长度
                                size_t sizeEx = dataSize + ourAdditionalDataLength;
                                uint8_t* finalFrameData = new uint8_t[sizeEx];
                                //复制原有数据
                                memcpy(finalFrameData, frameData.data(), dataSize);
                                // TODO End. Note the length of the frameid.
                                rtc::ArrayView<const uint8_t> frameDataEx(finalFrameData, sizeEx);
                                transformable_frame->SetData(frameDataEx);
                                if (this->sink_callback_) {
                                        this->sink_callback_->OnTransformedFrame(std::move(transformable_frame));
                                }

                                delete[] finalFrameData;
                                finalFrameData = nullptr;
                        }

                        void StreamDataTransformer::RegisterTransformedFrameCallback(rtc::scoped_refptr<webrtc::TransformedFrameCallback> callback)
                        {
                                this->sink_callback_ = callback;
                        }
                        void StreamDataTransformer::RegisterTransformedFrameSinkCallback(rtc::scoped_refptr<webrtc::TransformedFrameCallback> callback, uint32_t ssrc)
                        {
                                this->sink_callback_ = callback;
                        }
                        void StreamDataTransformer::UnregisterTransformedFrameCallback()
                        {
                                this->sink_callback_ = nullptr;
                        }
                        void StreamDataTransformer::UnregisterTransformedFrameSinkCallback(uint32_t ssrc)
                        {
                                this->sink_callback_ = nullptr;
                        }
}

要注意的

  1. 可插入帧是在帧的数据都处理完后扔出去的时候最终处理的,所以这里你可以做类似加密,压缩等行为。
  2. 但是此时,已经没有办法找到对应之前数据的内容了,因此你不可以在这个阶段做对齐的操作。
  3. 可插入帧的这个能力,并没有在所有的web浏览器下支持,请注意检查兼容性问题。

其余的可插入帧

“可插入帧”(插入帧或插入式编码)是一种增强视频编码和传输的技术,允许开发者在视频流中插入特定类型的帧,以实现各种目的,如提升视频质量、控制视频流的传输特性等。以下是对可插入帧能力的详细介绍以及其作用。

可插入帧的能力

  1. 插入关键帧(I帧)
    1. 定义:关键帧(I帧)是完整的图像帧,不依赖于其他帧。插入关键帧可以使视频流在某个时间点能够从该点完整解码。
    2. 作用
      • 恢复点:在网络条件不稳定或丢包的情况下,插入关键帧可以提供一个解码的恢复点。丢失数据包之后,如果接收到新的关键帧,解码器可以重新同步视频流。
      • 快速跳转:在视频播放器中,可以快速跳转到关键帧,帮助用户实现快速的随机访问。
      • 错误恢复:在发生传输错误时,通过插入关键帧,可以帮助恢复视频流的正常播放。
    3. 应用:例如,在直播流中,为了减少延迟和提高流的稳定性,可能会定期插入关键帧。或者,在需要保证视频质量的场景,如重要会议或高质量的视频会议中,也可以根据需要插入关键帧。
  2. 插入自适应帧
    1. 定义:自适应帧是根据网络状况或应用需求动态调整的帧。例如,根据带宽的变化调整帧率或质量。
    2. 作用
      • 适应网络条件:在带宽受限或网络条件不稳定时,插入自适应帧可以帮助调整视频流的质量,防止视频播放卡顿。
      • 动态调整:根据网络条件或应用场景的变化动态调整帧类型和质量,以实现最佳的用户体验。
  3. 插入修正帧
    1. 定义:修正帧用于对视频流中的错误进行修正。例如,在检测到数据丢失或解码错误时,插入修正帧可以帮助纠正错误。
    2. 作用
      • 增强鲁棒性:提高视频流的鲁棒性,确保在发生数据丢失或网络波动时,视频流能够平滑播放。
      • 错误修正:在视频流传输过程中,能够修正传输错误,减少视觉上的瑕疵。

插入帧的实现

在WebRTC中,可以通过以下方式实现插入帧的能力:

  1. 使用RTCPeerConnection API:可以控制视频流的编码器设置,比如设置关键帧的间隔等。例如:
const peerConnection = new RTCPeerConnection();

// 修改编码器设置以插入关键帧
const senders = peerConnection.getSenders();
for (const sender of senders) {
    if (sender.track.kind === 'video') {
        const parameters = sender.getParameters();
        parameters.encodings.forEach(encoding => {
            encoding.keyFrameRequestDelay = 1000; // 请求关键帧的延迟
        });
        sender.setParameters(parameters);
    }
}
  1. 通过媒体流处理:使用Canvas API和WebRTC的MediaStream结合,可以实现帧的插入和处理。具体步骤包括:
    1. 从视频流中获取帧。
    2. 在Canvas上处理帧并生成新的帧。
    3. 将处理后的帧重新插入到视频流中。
  2. 示例代码(前文已提供)演示了如何使用Canvas API对视频流应用滤镜。
  3. 自定义视频编码器:在某些情况下,可能需要使用自定义的视频编码器来实现特定的帧插入策略。这通常涉及到更底层的操作,可能需要对视频编码库进行定制或使用专业的编码工具。

总结

可插入帧的能力在流式系统中具有重要作用,能够提升视频流的稳定性、质量和适应性。通过插入关键帧、自适应帧和修正帧等技术,可以实现更好的视频体验。流式系统提供了灵活的API和工具,使开发者能够根据需求控制视频流的行为和质量。

RA/SD 衍生者AI训练营。发布者:chris,转载请注明出处:https://www.shxcj.com/archives/6703

Like (0)
Previous 2024-09-30
Next 2024-09-30

相关推荐

发表回复

Please Login to Comment
本文授权以下站点有原版访问授权 https://www.shxcj.com https://www.2img.ai https://www.2video.cn