当前位置: 首页 > news >正文

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

一、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;}}}

相关文章:

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

一、SCRIPTDATAVALUE类型 从《音视频入门基础&#xff1a;FLV专题&#xff08;9&#xff09;——Script Tag简介》中可以知道&#xff0c;根据《video_file_format_spec_v10_1.pdf》第80到81页&#xff0c;SCRIPTDATAVALUE类型由一个8位&#xff08;1字节&#xff09;的Type和…...

jvm里的metaspace oom 排查问题思路-使用MAT

文章目录 metapace oom 问题排查点-1-duplicate class 数量排查点-2-classloader 数量一些可能迷惑人的方向 metapace oom 问题 metapace 问题都是由加载的class 太多&#xff0c;导致内存不够报出来的。 排查点-1-duplicate class 数量 首先看这里&#xff0c;通常一个clas…...

2025舜宇招聘【内推码】

【2025内推码】 DSwNQ9yu DSJXN8Mr 舜宇集团2025届全球校园招聘正式启动&#xff01;&#xff01;&#xff01; 专业需求&#xff1a;机械、自动化、电子、电气、通信、控制、测控、计算机、软件、物理、光学等专业&#xff1b; 工作地点&#xff1a;宁波余姚、浙江杭州、广东…...

APP自动化搭建与应用

APP自动化环境搭建 用于做APP端UI自动化&#xff0c;adb连接手机设备。 需要的工具java编辑器&#xff1a;jdk、Android-sdk软件开发工具组、appium的python客户端、nodes.js、夜神模拟器、apk包、uiautomatorviewer 第一步&#xff1a;安装sdk&#xff0c;里面包含建立工具bu…...

kafka-windows集群部署

kafka-windows集群部署目录 文章目录 kafka-windows集群部署目录前言一、复制出来四个kafka文件夹二、修改集群每个kafka的配置文件四、启动zookeeper&#xff0c;kafka集群 前言 部署本文步骤可以先阅读这一篇博客&#xff0c;这篇是关于单机kafka部署测试的。本文用到的文件…...

4个顶级的大模型推理引擎

LLM 在文本生成应用中表现出色&#xff0c;例如具有高理解度和流畅度的聊天和代码完成模型。然而&#xff0c;它们的庞大规模也给推理带来了挑战。基本推理速度很慢&#xff0c;因为 LLM 会逐个生成文本标记&#xff0c;需要对每个下一个标记进行重复调用。随着输入序列的增长&…...

Oracle中ADD_MONTHS()函数详解

文章目录 前言一、ADD_MONTHS()的语法二、主要用途三、测试用例总结 前言 在Oracle数据库中&#xff0c;ADD_MONTHS()函数用于在日期中添加指定的月数。 一、ADD_MONTHS()的语法 ADD_MONTHS(date, n) 其中&#xff0c;date是一个日期值&#xff0c;n是一个整数值&#xff0c…...

【SQL】掌握SQL查询技巧:高效数据整合与查询优化

目录 1. SQL 的基本构成2. SQL 联接&#xff08;JOIN&#xff09;2.1 内联接&#xff08;INNER JOIN&#xff09;2.2 外联接&#xff08;OUTER JOIN&#xff09;2.2.1 左外联接&#xff08;LEFT JOIN&#xff09;2.2.2 右外联接&#xff08;RIGHT JOIN&#xff09;2.2.3 全外联…...

一个月学会Java 第5天 控制结构

Day5 控制结构 这么叫可能有些就算有基础的人也看不懂&#xff0c;其实就是if-else、switch-case、for、while、do-while这几个&#xff0c;没基础的听到了这个也不要慌张&#xff0c;这几个是程序的基础&#xff0c;多多训练就好 第一章 顺序结构 这章其实没有什么好讲的&…...

世界职业院校技能大赛(大数据技术与应用)参赛项目介绍内容模拟示例参考

最近关注世界职业院校技能大赛的同学应该都知道了&#xff0c;比赛已经正式改为”世界职业院校技能大赛“了&#xff0c;不仅仅是名称变化&#xff0c;而且比赛的形式也发生了巨大的改革&#xff0c;2024年世界职业院校技能大赛设置42个赛道&#xff0c;要求各比赛项目提交项目…...

【Python】文件及目录

文章目录 概要一、文件对象的函数1.1 open()函数1.2 文件对象的函数1.3 with语句 二、基于os和os.path模块的目录操作三、基于Pandas的文件处理3.1 Pandas读写各种类型文件 其他章节的内容 概要 本文主要将了打开文件的函数open()的参数&#xff0c;以及文件对象的函数&#x…...

OpenHarmony(鸿蒙南向开发)——标准系统方案之瑞芯微RK3566移植案例(下)

往期知识点记录&#xff1a; 鸿蒙&#xff08;HarmonyOS&#xff09;应用层开发&#xff08;北向&#xff09;知识点汇总 鸿蒙&#xff08;OpenHarmony&#xff09;南向开发保姆级知识点汇总~ 持续更新中…… 概述 OpenHarmony Camera驱动模型结构 HDI Implementation&#x…...

霓虹灯数字时钟(可复制源代码)

文章目录 一、效果演示二、CodeHTMLCSSJavaScript 三、实现思路拆分CSS 部分JavaScript 部分 四、源代码 一、效果演示 文末可一键复制完整代码 二、Code HTML <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><…...

大模型微调技术之 LoRA:开启高效微调新时代

一、LoRA 简介 LoRA&#xff0c;即低秩适应&#xff08;Low-Rank Adaptation&#xff09;&#xff0c;是一种用于微调大型语言模型的技术&#xff0c;旨在以较小的计算资源和数据量实现模型的快速适应特定任务或领域。 LoRA 方法通过引入低秩近似的思想&#xff0c;对大型预训…...

【Vue】Vue2(2)

文章目录 1 数据代理1.1 回顾Object.defineproperty方法1.2 何为数据代理1.3 Vue中的数据代理 2 事件处理2.1 事件的基本使用2.2 事件修饰符2.3 键盘事件 1 数据代理 1.1 回顾Object.defineproperty方法 <!DOCTYPE html> <html><head><meta charset&quo…...

如何实现一个基于 HTML+CSS+JS 的任务进度条

如何实现一个基于 HTMLCSSJS 的任务进度条 在网页开发中&#xff0c;任务进度条是一种常见的 UI 组件&#xff0c;它可以直观地展示任务的完成情况。本文将向你展示如何使用 HTML CSS JavaScript 来创建一个简单的、交互式的任务进度条。用户可以通过点击进度条的任意位置来…...

学会流体力学,冬天洗澡再也不冷啦

前些日子收到一位网友“究极理性怪物”的私信&#xff0c;说最近在学校的公共浴室洗澡时&#xff0c;快被冻死了&#xff0c;希望我从流体力学角度帮他分析一下浴室的温度分布&#xff0c;以便找到相对温暖的洗澡位置。 我看到后觉得很有意思&#xff0c;就与他展开了关于澡堂…...

WPF下使用FreeRedis操作RedisStream实现简单的消息队列

Redis Stream简介 Redis Stream是随着5.0版本发布的一种新的Redis数据类型: 高效消费者组:允许多个消费者组从同一数据流的不同部分消费数据,每个消费者组都能独立地处理消息,这样可以并行处理和提高效率。 阻塞操作:消费者可以设置阻塞操作,这样它们会在流中有新数据…...

踩坑NVTX

最开始在 【简说】NVTX Nsight Nvidia性能分析利器 看到NVTX的时候&#xff0c;我觉得这是一个好东西啊&#xff0c;可以详细说明每一段时间对应的是哪一段程序。 看了一下github&#xff0c;他的文章已经过时&#xff0c;现在已经不需要链接动态库了&#xff0c;直接includ…...

Ubuntu修改IP方法

方法一&#xff1a;通过图形化界面修改IP 打开网络设置&#xff1a; 点击桌面右上角的网络图标&#xff0c;然后选择“设置”或“网络设置”。 选择网络接口&#xff1a; 在网络设置窗口中&#xff0c;选择你正在使用的网络接口&#xff08;有线或无线网络&#xff09;。 进…...

C++——STL简介

目录 一、什么是STL 二、STL的版本 三、STL的六大组件 没用的话..... 不知不觉两个月没写博客了&#xff0c;暑假后期因为学校的事情在忙&#xff0c;开学又在准备学校的java免修&#xff0c;再然后才继续开始学C&#xff0c;然后最近打算继续写博客沉淀一下最近学到的几周…...

[linux] 磁盘清理相关

在 CentOS 7 中清理磁盘空间可以通过多种方法实现&#xff0c;以下是一些常用的步骤和命令&#xff1a; 1. 查找和删除大文件 你可以使用 find 命令查找占用大量空间的文件&#xff1a; find / -type f -size 100M 2>/dev/null这条命令会查找大于 100 MB 的文件。你可以根…...

【笔记】DDD领域驱动设计

同名读书笔记&#xff0c;对于一些自觉重要的点进行记录。 扩展资源&#xff1a;github.com/evancyz/ddd-learning UML中类图的一些基本知识 - jack_Meng - 博客园 最后的第四部分暂时没看 Part Two 模型驱动设计的构造块 Chapter 5 软件中所表示的模型 5.2 模式&#xff1a;…...

用AI构建小程序需要多久?效果如何?

随着移动互联网的快速发展&#xff0c;多端应用的需求日益增长。为了提高开发效率、降低成本并保证用户体验的一致性&#xff0c;前端跨端技术在如今的开发界使用已经非常普遍了&#xff0c;技术界较为常用的跨端技术有小程序技术、HTML5技术两大类。 2023年以来&#xff0c;伴…...

深度学习的应用综述

文章目录 引言深度学习的基本概念深度学习的主要应用领域计算机视觉自然语言处理语音识别强化学习医疗保健金融分析 深度学习应用案例公式1.损失函数(Loss Function) 结论 引言 深度学习是机器学习的一个子领域&#xff0c;通过模拟人脑的神经元结构来处理复杂的数据。近年来&…...

whereis命令:查找命令的路径

一、命令简介 ​whereis​ 命令用于查找命令的&#xff1a;可执行文件、帮助文件和源代码文件。 例如 $ whereis ls ls: /usr/bin/ls /usr/share/man/man1/ls.1.gz找到了 ls 命令的可执行文件、帮助文件的位置。 ‍ 二、命令参数 命令格式 whereis [选项] [命令名称]选项…...

【ECMAScript 从入门到进阶教程】第四部分:项目实践(项目结构与管理,单元测试,最佳实践与开发规范,附录)

第四部分&#xff1a;项目实践 第十四章 项目结构与管理 在构建现代 Web 应用程序时&#xff0c;良好的项目结构和管理是确保代码可维护性、高效开发和部署成功的关键因素。这一章将深入讨论项目初始化与配置&#xff0c;以及如何使用构建工具来简化和优化项目建设过程。 14…...

算法讲解—最小生成树(Kruskal 算法)

算法讲解—最小生成树&#xff08;Kruskal 算法&#xff09; 简介 根据度娘的解释我们可以知道&#xff0c;最小生成树(Minimum Spanning Tree, MST)就是&#xff1a;一个有 n n n 个结点的连通图的生成树是原图的极小连通子图&#xff0c;且包含原图中的所有 n n n 个结点…...

掌握 C# 多线程与异步编程

现代应用程序通常需要执行复杂的计算或处理 I/O 操作&#xff0c;这些操作可能会导致主线程阻塞&#xff0c;从而降低用户体验。C# 提供了多线程与异步编程的多种工具&#xff0c;让我们能够高效地并发处理任务。本文将介绍 C# 中的多线程与异步编程&#xff0c;包括 Thread 类…...

Angular 2 用户输入

Angular 2 用户输入 Angular 2 是一个由 Google 维护的开源前端 web 框架,用于构建单页应用程序(SPA)。它以其高效的双向数据绑定、模块化架构和强大的依赖注入系统而闻名。在 Angular 2 应用程序中,处理用户输入是核心功能之一,因为它允许应用程序响应用户的操作。 Ang…...