音视频入门基础:FLV专题(13)——FFmpeg源码中,解析任意Type值的SCRIPTDATAVALUE类型的实现

server/2024/10/9 1:12:34/

一、SCRIPTDATAVALUE类型

从《音视频入门基础:FLV专题(9)——Script Tag简介》中可以知道,根据《video_file_format_spec_v10_1.pdf》第80到81页,SCRIPTDATAVALUE类型由一个8位(1字节)的Type和一个ScriptDataValue组成。其中Type属性用来指定ScriptDataValue的类型,根据Type值的不同,ScriptDataValue的类型也不同:

二、ScriptTagBody

根据《video_file_format_spec_v10_1.pdf》第80页,ScriptTagBody包含以Action Message Format(AMF)编码的SCRIPTDATA。AMF是一种用于数据交换的开放标准,旨在简化数据传输和解析过程。ScriptTagBody由Name和Value组成,这两个属性都是SCRIPTDATAVALUE类型的:

ScriptTagBody = Name + Value

三、SCRIPTDATAOBJECTPROPERTY类型

ScriptTagBody的Value属性指定了AMF参数或对象的属性,为SCRIPTDATAVALUE类型。当Value对应的Type的值为8时,Value对应的ScriptDataValue属性为SCRIPTDATAECMAARRAY类型。

SCRIPTDATAECMAARRAY类型的Variables属性是一个数组,该数组的每个元素都为SCRIPTDATAOBJECTPROPERTY类型。数组中的每个元素由变量名称(PropertyName)和变量的值(PropertyData)组成。根据《video_file_format_spec_v10_1.pdf》第82页,PropertyName为SCRIPTDATASTRING类型,而PropertyData为SCRIPTDATAVALUE类型:

SCRIPTDATAOBJECTPROPERTY类型 = PropertyName + PropertyData

四、ScriptTagBody和SCRIPTDATAOBJECTPROPERTY类型的相似之处

从上面可以看出来,ScriptTagBody和SCRIPTDATAOBJECTPROPERTY类型,这两者的结构是非常相似的,它们都是由一个Name和一个Value/Data组成,特别是它们的第二个属性:Value和Data都是SCRIPTDATAVALUE类型。所以FFmpeg封装了一个通用的解析函数——amf_parse_object函数来实现解析ScriptTagBody的Value属性或SCRIPTDATAOBJECTPROPERTY类型的PropertyData属性。也就是说FFmpeg源码中,amf_parse_object函数是解析SCRIPTDATAVALUE类型的通用方法。不管是什么Type值的SCRIPTDATAVALUE类型都可以通过amf_parse_object函数把其Type和ScriptDataValue解析出来。

五、amf_parse_object函数的定义

amf_parse_object函数定义在FFmpeg源码(本文演示用的FFmpeg源码版本为7.0.1)的源文件libavformat/flvdec.c中:

static int amf_parse_object(AVFormatContext *s, AVStream *astream,AVStream *vstream, const char *key,int64_t max_pos, int depth)
{AVCodecParameters *apar, *vpar;FLVContext *flv = s->priv_data;AVIOContext *ioc;AMFDataType amf_type;FLVMetaVideoColor *meta_video_color = flv->metaVideoColor;char str_val[1024];double num_val;amf_date date;if (depth > MAX_DEPTH)return AVERROR_PATCHWELCOME;num_val  = 0;ioc      = s->pb;if (avio_feof(ioc))return AVERROR_EOF;amf_type = avio_r8(ioc);switch (amf_type) {case AMF_DATA_TYPE_NUMBER:num_val = av_int2double(avio_rb64(ioc));break;case AMF_DATA_TYPE_BOOL:num_val = avio_r8(ioc);break;case AMF_DATA_TYPE_STRING:if (amf_get_string(ioc, str_val, sizeof(str_val)) < 0) {av_log(s, AV_LOG_ERROR, "AMF_DATA_TYPE_STRING parsing failed\n");return -1;}break;case AMF_DATA_TYPE_OBJECT:if (key &&(ioc->seekable & AVIO_SEEKABLE_NORMAL) &&!strcmp(KEYFRAMES_TAG, key) && depth == 1)if (parse_keyframes_index(s, ioc, max_pos) < 0)av_log(s, AV_LOG_ERROR, "Keyframe index parsing failed\n");elseadd_keyframes_index(s);while (avio_tell(ioc) < max_pos - 2 &&amf_get_string(ioc, str_val, sizeof(str_val)) > 0)if (amf_parse_object(s, astream, vstream, str_val, max_pos,depth + 1) < 0)return -1;     // if we couldn't skip, bomb out.if (avio_r8(ioc) != AMF_END_OF_OBJECT) {av_log(s, AV_LOG_ERROR, "Missing AMF_END_OF_OBJECT in AMF_DATA_TYPE_OBJECT\n");return -1;}break;case AMF_DATA_TYPE_NULL:case AMF_DATA_TYPE_UNDEFINED:case AMF_DATA_TYPE_UNSUPPORTED:break;     // these take up no additional spacecase AMF_DATA_TYPE_MIXEDARRAY:{unsigned v;avio_skip(ioc, 4);     // skip 32-bit max array indexwhile (avio_tell(ioc) < max_pos - 2 &&amf_get_string(ioc, str_val, sizeof(str_val)) > 0)// this is the only case in which we would want a nested// parse to not skip over the objectif (amf_parse_object(s, astream, vstream, str_val, max_pos,depth + 1) < 0)return -1;v = avio_r8(ioc);if (v != AMF_END_OF_OBJECT) {av_log(s, AV_LOG_ERROR, "Missing AMF_END_OF_OBJECT in AMF_DATA_TYPE_MIXEDARRAY, found %d\n", v);return -1;}break;}case AMF_DATA_TYPE_ARRAY:{unsigned int arraylen, i;arraylen = avio_rb32(ioc);for (i = 0; i < arraylen && avio_tell(ioc) < max_pos - 1; i++)if (amf_parse_object(s, NULL, NULL, NULL, max_pos,depth + 1) < 0)return -1;      // if we couldn't skip, bomb out.}break;case AMF_DATA_TYPE_DATE:// timestamp (double) and UTC offset (int16)date.milliseconds = av_int2double(avio_rb64(ioc));date.timezone = avio_rb16(ioc);break;default:                    // unsupported type, we couldn't skipav_log(s, AV_LOG_ERROR, "unsupported amf type %d\n", amf_type);return -1;}if (key) {apar = astream ? astream->codecpar : NULL;vpar = vstream ? vstream->codecpar : NULL;// stream info doesn't live any deeper than the first objectif (depth == 1) {if (amf_type == AMF_DATA_TYPE_NUMBER ||amf_type == AMF_DATA_TYPE_BOOL) {if (!strcmp(key, "duration"))s->duration = num_val * AV_TIME_BASE;else if (!strcmp(key, "videodatarate") &&0 <= (int)(num_val * 1024.0))flv->video_bit_rate = num_val * 1024.0;else if (!strcmp(key, "audiodatarate") &&0 <= (int)(num_val * 1024.0))flv->audio_bit_rate = num_val * 1024.0;else if (!strcmp(key, "framerate")) {flv->framerate = av_d2q(num_val, 1000);if (vstream)vstream->avg_frame_rate = flv->framerate;} else if (flv->trust_metadata) {if (!strcmp(key, "videocodecid") && vpar) {int ret = flv_set_video_codec(s, vstream, num_val, 0);if (ret < 0)return ret;} else if (!strcmp(key, "audiocodecid") && apar) {int id = ((int)num_val) << FLV_AUDIO_CODECID_OFFSET;flv_set_audio_codec(s, astream, apar, id);} else if (!strcmp(key, "audiosamplerate") && apar) {apar->sample_rate = num_val;} else if (!strcmp(key, "audiosamplesize") && apar) {apar->bits_per_coded_sample = num_val;} else if (!strcmp(key, "stereo") && apar) {av_channel_layout_default(&apar->ch_layout, num_val + 1);} else if (!strcmp(key, "width") && vpar) {vpar->width = num_val;} else if (!strcmp(key, "height") && vpar) {vpar->height = num_val;} else if (!strcmp(key, "datastream")) {AVStream *st = create_stream(s, AVMEDIA_TYPE_SUBTITLE);if (!st)return AVERROR(ENOMEM);st->codecpar->codec_id = AV_CODEC_ID_TEXT;}}}if (amf_type == AMF_DATA_TYPE_STRING) {if (!strcmp(key, "encoder")) {int version = -1;if (1 == sscanf(str_val, "Open Broadcaster Software v0.%d", &version)) {if (version > 0 && version <= 655)flv->broken_sizes = 1;}} else if (!strcmp(key, "metadatacreator")) {if (   !strcmp (str_val, "MEGA")|| !strncmp(str_val, "FlixEngine", 10))flv->broken_sizes = 1;}}}if (meta_video_color) {if (amf_type == AMF_DATA_TYPE_NUMBER ||amf_type == AMF_DATA_TYPE_BOOL) {if (!strcmp(key, "colorPrimaries")) {meta_video_color->primaries = num_val;} else if (!strcmp(key, "transferCharacteristics")) {meta_video_color->transfer_characteristics = num_val;} else if (!strcmp(key, "matrixCoefficients")) {meta_video_color->matrix_coefficients = num_val;} else if (!strcmp(key, "maxFall")) {meta_video_color->max_fall = num_val;} else if (!strcmp(key, "maxCLL")) {meta_video_color->max_cll = num_val;} else if (!strcmp(key, "redX")) {meta_video_color->mastering_meta.r_x = num_val;} else if (!strcmp(key, "redY")) {meta_video_color->mastering_meta.r_y = num_val;} else if (!strcmp(key, "greenX")) {meta_video_color->mastering_meta.g_x = num_val;} else if (!strcmp(key, "greenY")) {meta_video_color->mastering_meta.g_y = num_val;} else if (!strcmp(key, "blueX")) {meta_video_color->mastering_meta.b_x = num_val;} else if (!strcmp(key, "blueY")) {meta_video_color->mastering_meta.b_y = num_val;} else if (!strcmp(key, "whitePointX")) {meta_video_color->mastering_meta.white_x = num_val;} else if (!strcmp(key, "whitePointY")) {meta_video_color->mastering_meta.white_y = num_val;} else if (!strcmp(key, "maxLuminance")) {meta_video_color->mastering_meta.max_luminance = num_val;} else if (!strcmp(key, "minLuminance")) {meta_video_color->mastering_meta.min_luminance = num_val;}}}if (amf_type == AMF_DATA_TYPE_OBJECT && s->nb_streams == 1 &&((!apar && !strcmp(key, "audiocodecid")) ||(!vpar && !strcmp(key, "videocodecid"))))s->ctx_flags &= ~AVFMTCTX_NOHEADER; //If there is either audio/video missing, codecid will be an empty objectif ((!strcmp(key, "duration")        ||!strcmp(key, "filesize")        ||!strcmp(key, "width")           ||!strcmp(key, "height")          ||!strcmp(key, "videodatarate")   ||!strcmp(key, "framerate")       ||!strcmp(key, "videocodecid")    ||!strcmp(key, "audiodatarate")   ||!strcmp(key, "audiosamplerate") ||!strcmp(key, "audiosamplesize") ||!strcmp(key, "stereo")          ||!strcmp(key, "audiocodecid")    ||!strcmp(key, "datastream")) && !flv->dump_full_metadata)return 0;s->event_flags |= AVFMT_EVENT_FLAG_METADATA_UPDATED;if (amf_type == AMF_DATA_TYPE_BOOL) {av_strlcpy(str_val, num_val > 0 ? "true" : "false",sizeof(str_val));av_dict_set(&s->metadata, key, str_val, 0);} else if (amf_type == AMF_DATA_TYPE_NUMBER) {snprintf(str_val, sizeof(str_val), "%.f", num_val);av_dict_set(&s->metadata, key, str_val, 0);} else if (amf_type == AMF_DATA_TYPE_STRING) {av_dict_set(&s->metadata, key, str_val, 0);} else if (   amf_type == AMF_DATA_TYPE_DATE&& isfinite(date.milliseconds)&& date.milliseconds > INT64_MIN/1000&& date.milliseconds < INT64_MAX/1000) {// timezone is ignored, since there is no easy way to offset the UTC// timestamp into the specified timezoneavpriv_dict_set_timestamp(&s->metadata, key, 1000 * (int64_t)date.milliseconds);}}return 0;
}

该函数作用就是解析SCRIPTDATAVALUE类型,把其Type属性和ScriptDataValue属性解析出来。

形参s:既是输入型参数也是输出型参数,指向一个AVFormatContext类型变量。执行amf_parse_object函数前,s->pb->buf_ptr必须指向待解析的SCRIPTDATAVALUE类型数据的开头,也就是其Type属性。如果要解析的是“onMetadata”中的元数据属性,执行amf_parse_object函数后,s->duration和(FLVContext *)(s->priv_data)会得到解析出来的部分属性(duration、videodatarate、audiodatarate等)。

形参astream:输出型参数,指向一个AVStream音频流对象,值一般为NULL。

形参vstream:输出型参数,指向一个AVStream视频流对象,值一般为NULL。

形参key:输入型参数。指向从SCRIPTDATASTRING类型的StringData属性中解析出来的字符串数据。

1.如果要解析的是ScriptTagBody的Value,key指向“从该ScriptTagBody的Name属性的StringData属性中解析出来的字符串数据”,比如:“onMetadata”。

2.如果要解析的是SCRIPTDATAOBJECTPROPERTY类型的PropertyData属性,key指向“从该SCRIPTDATAOBJECTPROPERTY类型的PropertyName属性的StringData属性中解析出来的字符串数据”,比如“duration”。

形参max_pos:输入型参数。所在Tag对应的PreviousTagSize相对于文件首的偏移(单位为字节)。

  

形参depth:输入型参数,表示该SCRIPTDATAVALUE类型数据的递归深度。该值从0开始,每递归调用自身的次数加1,depth的值加1。

返回值:返回一个负数表示解析失败;返回0表示解析成功。

六、amf_parse_object函数的内部实现原理

在前文中我们讲到,当SCRIPTDATAVALUE类型的Type值为8时,其ScriptDataValue属性为SCRIPTDATAECMAARRAY类型。SCRIPTDATAECMAARRAY类型的Variables属性是一个数组,该数组的每个元素都为SCRIPTDATAOBJECTPROPERTY类型。而SCRIPTDATAOBJECTPROPERTY类型的PropertyData属性又是SCRIPTDATAVALUE类型。

所以解析SCRIPTDATAVALUE类型需要用到递归,即amf_parse_object函数内部又会自我调用。形参depth表示该SCRIPTDATAVALUE类型数据的递归深度。该值从0开始,每递归调用自身的次数加1,depth的值加1。

宏定义MAX_DEPTH的值为16:

#define MAX_DEPTH 16      ///< arbitrary limit to prevent unbounded recursion

为了防止无限递归导致程序崩溃,amf_parse_object函数中首先判断递归深度是否大于16。如果大于16,返回AVERROR_PATCHWELCOME表示出错:

    if (depth > MAX_DEPTH)return AVERROR_PATCHWELCOME;

判断是否是因为读取到文件末尾而结束。如果是,返回AVERROR_EOF表示读取文件结束。关于avio_feof函数的用法可以参考:《FFmpeg源码:avio_feof函数分析》:

    ioc      = s->pb;if (avio_feof(ioc))return AVERROR_EOF;

读取SCRIPTDATAVALUE类型的Type属性。关于avio_r8函数的用法可以参考:《FFmpeg源码:avio_r8、avio_rl16、avio_rl24、avio_rl32、avio_rl64函数分析》。变量amf_type为枚举变量:

    AMFDataType amf_type;//...    amf_type = avio_r8(ioc);

枚举名称AMFDataType声明如下,对应SCRIPTDATAVALUE类型的Type属性:

typedef enum {AMF_DATA_TYPE_NUMBER      = 0x00,AMF_DATA_TYPE_BOOL        = 0x01,AMF_DATA_TYPE_STRING      = 0x02,AMF_DATA_TYPE_OBJECT      = 0x03,AMF_DATA_TYPE_NULL        = 0x05,AMF_DATA_TYPE_UNDEFINED   = 0x06,AMF_DATA_TYPE_REFERENCE   = 0x07,AMF_DATA_TYPE_MIXEDARRAY  = 0x08,AMF_DATA_TYPE_OBJECT_END  = 0x09,AMF_DATA_TYPE_ARRAY       = 0x0a,AMF_DATA_TYPE_DATE        = 0x0b,AMF_DATA_TYPE_LONG_STRING = 0x0c,AMF_DATA_TYPE_UNSUPPORTED = 0x0d,
} AMFDataType;

根据Type值的不同,执行不同的解析ScriptDataValue的方法:

    switch (amf_type) {case AMF_DATA_TYPE_NUMBER://...break;case AMF_DATA_TYPE_BOOL://...break;case AMF_DATA_TYPE_STRING://...break;case AMF_DATA_TYPE_OBJECT://...break;case AMF_DATA_TYPE_NULL:case AMF_DATA_TYPE_UNDEFINED:case AMF_DATA_TYPE_UNSUPPORTED:break;     // these take up no additional spacecase AMF_DATA_TYPE_MIXEDARRAY://...break;}case AMF_DATA_TYPE_ARRAY://...break;case AMF_DATA_TYPE_DATE://...default:                    // unsupported type, we couldn't skipav_log(s, AV_LOG_ERROR, "unsupported amf type %d\n", amf_type);return -1;}

下面分情况讨论:

(一)Type的值为0

当Type的值为0(即变量amf_type为AMF_DATA_TYPE_NUMBER)时,ScriptDataValue为DOUBLE类型。此时首先通过avio_rb64(ioc)读取8个字节, 然后通过av_int2double函数将这8个字节数据转换为double类型赋值给变量num_val。关于av_int2double函数的的用法可以参考:《音视频入门基础:FLV专题(12)——FFmpeg源码中,解析DOUBLE类型的ScriptDataValue的实现》:

    double num_val;//...case AMF_DATA_TYPE_NUMBER:num_val = av_int2double(avio_rb64(ioc));break;

(二)Type的值为2

当Type的值为2(即变量amf_type为AMF_DATA_TYPE_STRING)时,ScriptDataValue为SCRIPTDATASTRING类型。此时通过amf_get_string函数解析SCRIPTDATASTRING类型的ScriptDataValue,让数组str_val得到从SCRIPTDATASTRING类型的ScriptDataValue中解析出来的实际的字符串数据。关于amf_get_string函数的用法可以参考:《音视频入门基础:FLV专题(11)——FFmpeg源码中,解析SCRIPTDATASTRING类型的ScriptDataValue的实现》:

    char str_val[1024];//...case AMF_DATA_TYPE_STRING:if (amf_get_string(ioc, str_val, sizeof(str_val)) < 0) {av_log(s, AV_LOG_ERROR, "AMF_DATA_TYPE_STRING parsing failed\n");return -1;}break;

(三)Type的值为8

当Type的值为8(即变量amf_type为AMF_DATA_TYPE_MIXEDARRAY)时,ScriptDataValue为SCRIPTDATAECMAARRAY类型,执行下面的逻辑解析SCRIPTDATAECMAARRAY类型:

    case AMF_DATA_TYPE_MIXEDARRAY:{unsigned v;avio_skip(ioc, 4);     // skip 32-bit max array indexwhile (avio_tell(ioc) < max_pos - 2 &&amf_get_string(ioc, str_val, sizeof(str_val)) > 0)// this is the only case in which we would want a nested// parse to not skip over the objectif (amf_parse_object(s, astream, vstream, str_val, max_pos,depth + 1) < 0)return -1;v = avio_r8(ioc);if (v != AMF_END_OF_OBJECT) {av_log(s, AV_LOG_ERROR, "Missing AMF_END_OF_OBJECT in AMF_DATA_TYPE_MIXEDARRAY, found %d\n", v);return -1;}break;}

由《音视频入门基础:FLV专题(9)——Script Tag简介》可以知道,SCRIPTDATAECMAARRAY类型 = 4字节的ECMAArrayLength + Variables数组(ECMA数组) + List Terminator

“case AMF_DATA_TYPE_MIXEDARRAY”的代码块中,首先通过avio_skip函数跳过4个字节,即跳过SCRIPTDATAECMAARRAY类型的4字节ECMAArrayLength。关于avio_skip函数的用法可以参考:《FFmpeg源码:avio_skip函数分析》:

        avio_skip(ioc, 4);     // skip 32-bit max array index

解析SCRIPTDATAECMAARRAY类型的过程中,不通过ECMAArrayLength(该属性指定ECMA数组中的元素个数)判断是否结束解析。而是通过“avio_tell(ioc) < max_pos - 2”(文件位置指针当前位置相对于文件首的偏移 是否小于 所在Tag对应的PreviousTagSize相对于文件首的偏移 - 2)来判断是否要结束解析。通过语句amf_get_string(ioc, str_val, sizeof(str_val) 将Variables数组中某元素的PropertyName属性对应的字符串解析出来,存到str_val数组中:

        while (avio_tell(ioc) < max_pos - 2 &&amf_get_string(ioc, str_val, sizeof(str_val)) > 0)

通过递归,自我调用,解析Variables数组中某元素的PropertyData属性。如果解析失败,返回-1:

            // this is the only case in which we would want a nested// parse to not skip over the objectif (amf_parse_object(s, astream, vstream, str_val, max_pos,depth + 1) < 0)return -1;

宏定义AMF_END_OF_OBJECT的值为9:

#define AMF_END_OF_OBJECT         0x09

读取SCRIPTDATAECMAARRAY类型的List Terminator的最后一个字节,判断其是否等于9。由《音视频入门基础:FLV专题(9)——Script Tag简介》可以知道,List Terminator固定占3字节,值必须为0,0,9,所以List Terminator的最后一个字节必须为9。如果读取到不为9,打印日志:““Missing AMF_END_OF_OBJECT in AMF_DATA_TYPE_MIXEDARRAY, found”,amf_parse_object函数返回返回-1表示解析失败:

        v = avio_r8(ioc);if (v != AMF_END_OF_OBJECT) {av_log(s, AV_LOG_ERROR, "Missing AMF_END_OF_OBJECT in AMF_DATA_TYPE_MIXEDARRAY, found %d\n", v);return -1;}

(四)解析onMetaData里面的属性

由《音视频入门基础:FLV专题(9)——Script Tag简介》可以知道,

根据《video_file_format_spec_v10_1.pdf》第84页,FLV文件的元数据对象应该在名称为“onMetadata”的SCRIPTDATA tag(Script Tag)中携带。也就是说FLV文件中存在一个特殊的Script Tag,它的ScriptTagBody中的Name属性中的StringData属性存贮“onMetadata”字符串,而它的ScriptTagBody中的Value属性中的Variables数组中的每个元素都分别为一个元数据属性。用来存贮当前文件的一些基本信息,比如视频和音频的编解码器ID、视频的分辨率、文件大小、文件总时长、创建日期等:

amf_parse_object函数中通过下面代码块将名称为“onMetadata”的Script Tag中的部分属性(duration、videodatarate、audiodatarate等)解析出来,存到s->duration和(FLVContext *)(s->priv_data)中:

        // stream info doesn't live any deeper than the first objectif (depth == 1) {if (amf_type == AMF_DATA_TYPE_NUMBER ||amf_type == AMF_DATA_TYPE_BOOL) {if (!strcmp(key, "duration"))s->duration = num_val * AV_TIME_BASE;else if (!strcmp(key, "videodatarate") &&0 <= (int)(num_val * 1024.0))flv->video_bit_rate = num_val * 1024.0;else if (!strcmp(key, "audiodatarate") &&0 <= (int)(num_val * 1024.0))flv->audio_bit_rate = num_val * 1024.0;else if (!strcmp(key, "framerate")) {flv->framerate = av_d2q(num_val, 1000);if (vstream)vstream->avg_frame_rate = flv->framerate;} else if (flv->trust_metadata) {if (!strcmp(key, "videocodecid") && vpar) {int ret = flv_set_video_codec(s, vstream, num_val, 0);if (ret < 0)return ret;} else if (!strcmp(key, "audiocodecid") && apar) {int id = ((int)num_val) << FLV_AUDIO_CODECID_OFFSET;flv_set_audio_codec(s, astream, apar, id);} else if (!strcmp(key, "audiosamplerate") && apar) {apar->sample_rate = num_val;} else if (!strcmp(key, "audiosamplesize") && apar) {apar->bits_per_coded_sample = num_val;} else if (!strcmp(key, "stereo") && apar) {av_channel_layout_default(&apar->ch_layout, num_val + 1);} else if (!strcmp(key, "width") && vpar) {vpar->width = num_val;} else if (!strcmp(key, "height") && vpar) {vpar->height = num_val;} else if (!strcmp(key, "datastream")) {AVStream *st = create_stream(s, AVMEDIA_TYPE_SUBTITLE);if (!st)return AVERROR(ENOMEM);st->codecpar->codec_id = AV_CODEC_ID_TEXT;}}}if (amf_type == AMF_DATA_TYPE_STRING) {if (!strcmp(key, "encoder")) {int version = -1;if (1 == sscanf(str_val, "Open Broadcaster Software v0.%d", &version)) {if (version > 0 && version <= 655)flv->broken_sizes = 1;}} else if (!strcmp(key, "metadatacreator")) {if (   !strcmp (str_val, "MEGA")|| !strncmp(str_val, "FlixEngine", 10))flv->broken_sizes = 1;}}}


http://www.ppmy.cn/server/129016.html

相关文章

【MySQL】Ubuntu环境下MySQL的安装与卸载

目录 1.MYSQL的安装 2.MySQL的登录 3.MYSQL的卸载 4.设置配置文件 1.MYSQL的安装 首先我们要看看我们环境里面有没有已经安装好的MySQL 我们发现是默认是没有的。 我们还可以通过下面这个命令来确认有没有mysql的安装包 首先我们得知道我们当前的系统版本是什么 lsb_…

【Docker从入门到进阶】01.介绍 02.基础使用

1. 介绍 1.1. 什么是 Docker Docker 是一个开源的平台&#xff0c;用于开发、发布和运行应用程序。它使开发者能够以更精简的方式封装应用及其依赖&#xff0c;做到“打包一次&#xff0c;到处运行”。通过 Docker&#xff0c;您可以创建轻量级、可移植的容器&#xff0c;每个…

使用MTVerseXR SDK实现VR串流

1、概述​ MTVerseXR SDK 是摩尔线程GPU加速的虚拟现实&#xff08;VR&#xff09;流媒体平台&#xff0c;专门用于从远程服务器流式传输基于标准OpenXR的应用程序。MTVerseXR可以通过Wi-Fi和USB流式将VR内容从Windows服务器流式传输到XR客户端设备, 使相对性能低的VR客户端可…

SQL第12课——联结表

三点&#xff1a;什么是联结&#xff1f;为什么使用联结&#xff1f;如何编写使用联结的select语句 12.1 联结 SQL最强大的功能之一就是能在数据查询的执行中联结&#xff08;join)表。联结是利用SQL的select能执行的最重要的操作。 在使用联结前&#xff0c;需要了解关系表…

银河麒麟,apt 安装软件报错640Unknown Status

今天把银行麒麟的机器恢复出厂了&#xff0c;然后apt install 安装极其不稳定&#xff0c;故障现象如下图所示&#xff1a; 错误提示里面有&#xff1a; 640 Unknown Status [IP: 106.116.184.122 80] E: 无法下载 http://archive.kylinos.cn/kylin/KYLIN-ALL/pool/universe/f…

序列化与反序列化基础及反序列化漏洞(附案例)

参考文章&#xff1a; [web安全原理]PHP反序列化漏洞 - 笑花大王 - 博客园 (cnblogs.com) 一、概念 为了能有效的存储数据而不丢失数据的类型和内容&#xff0c;经常需要通过序列化对数据进行处理&#xff0c;将数据进行序列化后&#xff0c;会生成一个字符串&#xff0c;字符…

Go语言实现长连接并发框架 - 消息

文章目录 前言接口结构体接口实现项目地址最后 前言 你好&#xff0c;我是醉墨居士&#xff0c;上篇博客实现了对连接的封装&#xff0c;那我们对连接的读写操作涉及数据格式的定义&#xff0c;我们采用统一的数据格式。使用我们这种数据格式的数据&#xff0c;我们将其称之为…

17 链表——21. 合并两个有序链表 ★

17 链表 21. 合并两个有序链表 将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例 1: 输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4] 算法设计: 合并两个有序链表,并保持有序性,可以采用迭代法和递归法两种…