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

news/2024/10/9 4:42:42/

一、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/news/1536466.html

相关文章

CMU 10423 Generative AI:lec15(Scaling Laws 大规模语言模型的扩展法则)

文章目录 一 概述1. **扩展规律的背景**2. **两种主要的扩展规律**3. **模型容量扩展规律**4. **信息论下界**5. **计算扩展规律**6. **训练高效性**7. **结论与启示** 二 2bit/parameter 概念&#xff08;模型的存储能力分析&#xff09;**1. 概念解释****2. 图表解读****3. 量…

【Git原理与使用】远程操作标签管理

远程操作&&标签管理 1.理解分布式版本控制系统2.新建远程仓库3.克隆远程仓库4.向远程仓库推送5.拉取远程仓库6.配置 Git7.配置命令别名8.标签管理8.1创建标签8.2操作标签 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496;…

【Kubernetes】常见面试题汇总(五十六)

目录 123. pod 创建失败&#xff1f; 124. kube-flannel-ds-amd64-ndsf7 插件 pod 的 status 为 Init:0/1 &#xff1f; 特别说明&#xff1a; 题目 1-68 属于【Kubernetes】的常规概念题&#xff0c;即 “ 汇总&#xff08;一&#xff09;~&#xff08;二十二&#x…

今日指数项目项目集成RabbitMQ与CaffienCatch

今日指数项目项目集成RabbitMQ与CaffienCatch 一. 为什么要集成RabbitMQ 首先CaffeineCatch 是作为一个本地缓存工具 使用CaffeineCatch 能够大大较少I/O开销 股票项目 主要分为两大工程 --> job工程(负责数据采集) , backend(负责业务处理) 由于股票的实时性也就是说 ,…

Valhalla实现 -Docker部署利用OSM(Mapbox)地图实现路径规划可视化

一. Valhalla基本概念 1. 背景介绍&#xff1a; 官网介绍文档&#xff1a;https://valhalla.github.io/valhalla/ Valhalla是一个开源的路由引擎&#xff0c;能够实现实时路径规划&#xff0c;处理大量请求返回最优路径。 基于 OSM 数据&#xff0c;结合灵活的多模式交通方式…

Spring Boot:打造下一代医院管理系统

3系统分析 3.1可行性分析 通过对本医院管理系统实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本医院管理系统采用JAVA作为开发语言&#xff0c;Spring Boot框…

Pikachu-url重定向-不安全的url跳转

不安全的url跳转 不安全的url跳转问题可能发生在一切执行了url地址跳转的地方。如果后端采用了前端传进来的(可能是用户传参,或者之前预埋在前端页面的url地址)参数作为了跳转的目的地,而又没有做判断的话就可能发生"跳错对象"的问题。 url跳转比较直接的危害是: …

Apache Flink 和 Apache Kafka

Apache Flink 和 Apache Kafka 都是大数据生态系统中非常重要的工具&#xff0c;但它们的作用和应用场景有所不同。下面将分别介绍两者的主要特性和它们之间的异同点。 Apache Kafka 作用&#xff1a; 消息队列&#xff1a;Kafka 主要作为消息队列使用&#xff0c;用于解耦生…