OBS编译SEI功能开发记录

如何编译OBS

  1. 下载源码。

  2. 安装dxSDK。安装失败也不要紧。主要是生成依赖的dll文件。

  3. 解压QT到“C:\QtDep”目录下。编译的时候会添加QT的依赖项。

  4. 在obs-studio目录下运行CI/install-script-win-nodownload.cmd文件。

5a50292d-75c2-4501-8b11-9a491356724d.jpg
  1. 打开obs-studio\build64\obs-studio.sln文件工程编译即可。

OBS开发基础

几乎所有自定义功能都是通过插件模块添加的,这些插件模块通常是动态库或脚本。捕获和/或输出音频/视频、录制、输出到RTMP流、用x264编码的能力都是通过插件模块完成的。
插件可以实现源、输出、编码器和服务。


image.png

Sources

源用于在流上呈现视频和/或音频。诸如捕获显示/游戏/音频,播放视频,显示图像或播放音频之类的事情。源也可以用来实现音频和视频过滤器以及过渡。libobs/obs-source.h文件是用于实现源代码的专用头文件。有关更多信息,请参阅源API参考(obs_source_t)。
例如,要实现一个源对象,你需要定义一个obs_source_info结构并填充与源相关的信息和回调

Outputs

输出允许输出当前渲染的音频/视频的能力。流媒体和录音是输出的两个常见示例,但不是唯一的输出类型。输出可以接收原始数据或接收编码数据。libobs/obs-output.h文件是实现输出的专用头文件。有关更多信息,请参阅输出API参考(obs_output_t)。
例如,要实现一个输出对象,你需要定义一个obs_output_info结构,并用与输出相关的信息和回调填充它:

Encoders

编码器是视频/音频编码器的obs特定实现,它与使用编码器的输出一起使用。x264, NVENC, Quicksync是编码器实现的例子。libobs/obs-encoder.h文件是实现编码器的专用头文件。有关更多信息,请参阅编码器API参考(obs_encoder_t)。
例如,要实现一个编码器对象,你需要定义一个obs_encoder_info结构并填充与编码器相关的信息和回调:

Serveices

服务是流服务的自定义实现,它与流输出一起使用。例如,你可以有一个自定义的实现流到Twitch,另一个YouTube允许登录和使用他们的api做的事情,如获得RTMP服务器或控制频道。libbs /obs-service.h文件是实现服务的专用头文件。有关更多信息,请参阅服务API参考(obs_service_t)。
例如,要实现一个服务对象,你需要定义一个obs_service_info结构,并填充与你的服务相关的信息和回调:

假如我希望obs用cuda来硬编h264,我们只需要做2步即可

  1. 定义obs_encoder_info 的struct。然后注册进去。
#include <obs-module.h>

OBS_DECLARE_MODULE()
OBS_MODULE_USE_DEFAULT_LOCALE("obs-x264", "en-US")
MODULE_EXPORT const char *obs_module_description(void)
{
        return "x264 based encoder";
}

extern struct obs_encoder_info obs_x264_encoder;

bool obs_module_load(void)
{
        obs_register_encoder(&obs_x264_encoder);
        return true;
}
  1. 实现encoder对应的函数
struct obs_encoder_info obs_x264_encoder = {
        .id = "obs_x264",
        .type = OBS_ENCODER_VIDEO,
        .codec = "h264",
        .get_name = obs_x264_getname,
        .create = obs_x264_create,
        .destroy = obs_x264_destroy,
        .encode = obs_x264_encode,
        .update = obs_x264_update,
        .get_properties = obs_x264_props,
        .get_defaults = obs_x264_defaults,
        .get_extra_data = obs_x264_extra_data,
        .get_sei_data = obs_x264_sei,
        .get_video_info = obs_x264_video_info,
        .caps = OBS_ENCODER_CAP_DYN_BITRATE,
};

static bool obs_x264_encode(void *data, struct encoder_frame *frame,
                            struct encoder_packet *packet,
                            bool *received_packet)
{
        struct obs_x264 *obsx264 = data;
        x264_nal_t *nals;
        int nal_count;
        int ret;
        x264_picture_t pic, pic_out;

        if (!frame || !packet || !received_packet)
                return false;

        if (frame)
                init_pic_data(obsx264, &pic, frame);

        ret = x264_encoder_encode(obsx264->context, &nals, &nal_count,
                                  (frame ? &pic : NULL), &pic_out);
        if (ret < 0) {
                warn("encode failed");
                return false;
        }

        *received_packet = (nal_count != 0);
        parse_packet(obsx264, packet, nals, nal_count, &pic_out);

        return true;
}

OBS插入自定义SEI

从关键代码分析看会发现仅仅靠目前的OBS Plugin无法自定义插入SEI。他仅仅是第一帧才发SEI。
了解SEI


image.png

序列参数集 SPS----7:
SPS即Sequence Paramater Set SPS中保存了一组编码视频序列(Coded video sequence)的全局参数。所谓的编码视频序列即原始视频的一帧一帧的像素数据经过编码之后的结构组成的序列。而每一帧的编码后数据所依赖的参数保存于图像参数集中。一般情况SPS和PPS的NAL Unit通常位于整个码流的起始位置。但在某些特殊情况下,在码流中间也可能出现这两种结构,主要原因可能为:
图像参数集 PPS----8:
除了序列参数集SPS之外,H.264中另一重要的参数集合为图像参数集Picture Paramater Set(PPS)。通常情况下,PPS类似于SPS,在H.264的裸码流中单独保存在一个NAL Unit中,只是PPS NAL Unit的nal_unit_type值为8;而在封装格式中,PPS通常与SPS一起,保存在视频文件的文件头中。
关键帧 IDR 帧----5:
I帧表示关键帧,你可以理解为这一帧画面的完整保留;解码时只需要本帧数据就可以完成(因为包含完整画面)
P帧 ----1:
P帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。(也就是差别帧,P帧没有完整画面数据,只有与前一帧的画面差别的数据)

添加sei信息后帧头部构成,P帧头部构成如下,I帧前面还有一些别的信息
起始码 0x00000001
NAL header 0x06 SEI类型
SEI payload type 0x05 遵循user_data_unregistered()语法
SEI payload size 可以上下浮动 即uuid_size+content_size
SEI payload uuid 一般为16
SEI payload content 结束符"0x00" 前面为有效数据
rbsp trailing bits 0x80 NAL unit结尾写入的字节一定是0x80

注:sei信息没有长度限制,SEI payload size是unsigned char类型,当content很长的时候,

插入SEI

输入流读取一个packet,若为video frame,添加sei信息,将packet输出。

static void send_sei_video_packet(struct obs_encoder *encoder,
                                  struct encoder_callback *cb,
                                  struct encoder_packet *packet,
                                  uint8_t *sei_info)
{
        struct encoder_packet targetPacket;
        DARRAY(uint8_t) data;

        /* always wait for first keyframe */
        /*if (!packet->keyframe) {
                cb->new_packet(cb->param, packet);
                return;
        }*/

        da_init(data);

        /*if (!get_sei(encoder, &sei, &size) || !sei || !size) {
                cb->new_packet(cb->param, packet);
                cb->sent_first_packet = true;
                return;
        }*/

        uint32_t contineSize;
        uint8_t *contine = sei_info;
        uint32_t inputSize = strlen(contine);
        contineSize = get_sei_packet_size(inputSize);
        uint8_t *sei = (uint8_t *)malloc(contineSize * sizeof(uint8_t));
        fill_sei_packet(sei, true, contine, inputSize);

        da_push_back_array(data, sei, contineSize);
        da_push_back_array(data, packet->data, packet->size);

        targetPacket = *packet;
        targetPacket.data = data.array;
        targetPacket.size = data.num;
        cb->new_packet(cb->param, &targetPacket);
        //cb->sent_first_packet = true;

        //free(sei);
        da_free(data);
}
封装SEI信息
int fill_sei_packet(unsigned char *packet, bool isAnnexb, const char *content,
                    uint32_t size)
{
        unsigned char *data = (unsigned char *)packet;
        unsigned int nalu_size = (unsigned int)get_sei_nalu_size(size);
        uint32_t sei_size = nalu_size;
        //大端转小端
        nalu_size = reversebytes(nalu_size);

        //NALU开始码
        unsigned int *size_ptr = &nalu_size;
        if (isAnnexb) {
                memcpy(data, start_code, sizeof(unsigned int));
                data += sizeof(unsigned int);
        } else {
                memcpy(data, size_ptr, sizeof(unsigned int));
                data += sizeof(unsigned int);
        }

        unsigned char *sei = data;
        //NAL header
        *data++ = 6; //SEI
        //sei payload type
        *data++ = 5; //unregister
        size_t sei_payload_size = size;

        //blog(LOG_INFO, "fill_sei_packet@sei start code : %lx %lx %lx %lx %lx %lx" , packet[0], packet[1],
        //     packet[2], packet[3], packet[4], packet[5]);
        //数据长度
        while (true) {
                *data++ = (sei_payload_size >= 0xFF ? 0xFF
                                                    : (char)sei_payload_size);
                if (sei_payload_size < 0xFF)
                        break;
                sei_payload_size -= 0xFF;
        }
        //此处不写入uuid。
        //UUID
        /*memcpy(data, uuid, UUID_SIZE);
        data += UUID_SIZE;*/
        //数据
        memcpy(data, content, size);
        data += size;

        //tail stop code
        if (sei + sei_size - data == 1) {
                *data = 0x80;
        } else if (sei + sei_size - data == 2) {
                *data++ = 0x00;
                *data++ = 0x80;
        }
        return true;
}

调试工具

8e72f042-1dda-4173-afc6-63241e65908d.jpg

| forbidden_zero_bit | nal_ref_idc | nal_unit_type |
--------------------+-------------+---------------
| 1 bit | 2 bit | 5 bit |

  • 合作方反馈收到sei前面包含部分乱码。当时猜测就是拼接的时候多传了uuid。
e143d65c-eb79-469d-b663-6afdfdedd4f2.jpg
  • Wireshark 抓取发现自己拼接的16位uuid被SDK转成string返回给了接口。于是删除了uuid的拼接。(删除后某些流媒体会认为这是一个不合格的sei信息。)
d90151ea-01ab-480d-9ee4-92fe6749930a.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容