以下是利用 libflv 库解析 FLV 文件大小和视频时间长度的 C 程序。
/** * flv_info.c * 使用 libflv 库解析 FLV 文件,获取文件大小和视频时长 * * 编译命令: * gcc -o flv_info flv_info.c -lflv -lpthread * * 交叉编译示例 (RV1106): * arm-rockchip830-linux-uclibcgnueabihf-gcc -o flv_info flv_info.c -I./include -L./lib -lflv -lpthread */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <sys/stat.h> /* libflv 头文件 - 根据实际库的 API 调整 */ #include "libflv/flv.h" /* FLV Tag 类型定义 */ #define FLV_TAG_TYPE_AUDIO 0x08 #define FLV_TAG_TYPE_VIDEO 0x09 #define FLV_TAG_TYPE_SCRIPT 0x12 /* FLV 文件头大小 (9 bytes) */ #define FLV_HEADER_SIZE 9 /* Tag 头大小 (11 bytes) */ #define FLV_TAG_HEADER_SIZE 11 /* PreviousTagSize 字段大小 (4 bytes) */ #define PREV_TAG_SIZE_SIZE 4 /* 解析结果结构体 */ typedef struct { char filename[256]; /* 文件名 */ long file_size; /* 文件大小 (字节) */ double duration; /* 视频时长 (秒) */ uint32_t video_track; /* 是否有视频轨 (1=有, 0=无) */ uint32_t audio_track; /* 是否有音频轨 (1=有, 0=无) */ uint32_t video_codec; /* 视频编码类型 */ uint32_t audio_codec; /* 音频编码类型 */ uint32_t width; /* 视频宽度 (从 metadata 读取) */ uint32_t height; /* 视频高度 (从 metadata 读取) */ double framerate; /* 帧率 (从 metadata 读取) */ uint32_t sample_rate; /* 音频采样率 */ } FlvInfo; /* 获取文件大小 */ static long get_file_size(const char* filename) { struct stat st; if (stat(filename, &st) != 0) { return -1; } return st.st_size; } /* 解析 FLV 文件头,验证文件格式并获取基本信息 */ static int parse_flv_header(FILE* fp, FlvInfo* info) { uint8_t header[FLV_HEADER_SIZE]; /* 读取 FLV 头 */ if (fread(header, 1, FLV_HEADER_SIZE, fp) != FLV_HEADER_SIZE) { fprintf(stderr, "Error: Failed to read FLV header\n"); return -1; } /* 验证 FLV 签名 "FLV" */ if (header[0] != 'F' || header[1] != 'L' || header[2] != 'V') { fprintf(stderr, "Error: Not a valid FLV file (invalid signature)\n"); return -1; } /* 版本号 */ uint8_t version = header[3]; printf("FLV Version: %d\n", version); /* TypeFlagsReserved + TypeFlagsAudio + TypeFlagsVideo * header[4] 第4位: 视频存在标志, 第2位: 音频存在标志 */ uint8_t flags = header[4]; info->video_track = (flags & 0x04) ? 1 : 0; info->audio_track = (flags & 0x01) ? 1 : 0; /* DataOffset: header 大小,通常是 9 */ uint32_t data_offset = (header[5] << 16) | (header[6] << 8) | header[7]; printf("Data offset: %d bytes\n", data_offset); printf("Video track: %s\n", info->video_track ? "Yes" : "No"); printf("Audio track: %s\n", info->audio_track ? "Yes" : "No"); /* 跳转到第一个 Tag 数据位置 */ if (data_offset > FLV_HEADER_SIZE) { fseek(fp, data_offset - FLV_HEADER_SIZE, SEEK_CUR); } return 0; } /* 解析 FLV Tag 头,获取 Tag 类型和数据大小 */ static int parse_tag_header(FILE* fp, uint8_t* tag_type, uint32_t* data_size, uint32_t* timestamp) { uint8_t header[FLV_TAG_HEADER_SIZE]; if (fread(header, 1, FLV_TAG_HEADER_SIZE, fp) != FLV_TAG_HEADER_SIZE) { return -1; } /* Reserved + Filter + TagType (第1字节) */ *tag_type = header[0]; /* DataSize: 24位大端整数 */ *data_size = (header[1] << 16) | (header[2] << 8) | header[3]; /* Timestamp: 24位低8位 + 8位扩展位 = 32位 */ *timestamp = (header[4] << 16) | (header[5] << 8) | header[6]; *timestamp |= (header[7] << 24); /* StreamID: 24位,通常为0,跳过 */ return 0; } /* 解析 onMetaData 脚本数据,提取时长、宽高等信息 */ static int parse_metadata(const uint8_t* data, uint32_t size, FlvInfo* info) { /* * 注意: 这是一个简化版的 metadata 解析示例。 * 完整的 AMF (Action Message Format) 解析需要处理多种数据类型: * - AMF_TYPE_NUMBER (0x00): 8字节双精度浮点数 * - AMF_TYPE_BOOLEAN (0x01): 1字节布尔值 * - AMF_TYPE_STRING (0x02): 2字节长度 + UTF-8字符串 * - AMF_TYPE_OBJECT (0x03): 键值对集合 * - AMF_TYPE_ECMA_ARRAY (0x08): 关联数组 * - AMF_TYPE_END (0x09): 对象结束标记 * * 实际使用中建议使用专门的 AMF 解析库。 */ printf(" [Metadata] size: %u bytes\n", size); /* 简化处理: 在实际代码中,这里应该完整解析 AMF 数据 * 读取 duration、width、height、framerate 等字段 */ return 0; } /* 解析视频 Tag 数据,获取编码类型 */ static int parse_video_tag(const uint8_t* data, uint32_t size, FlvInfo* info) { if (size < 1) return -1; /* FrameType (高4位) + CodecID (低4位) */ uint8_t frame_and_codec = data[0]; uint8_t frame_type = (frame_and_codec >> 4) & 0x0F; uint8_t codec_id = frame_and_codec & 0x0F; info->video_codec = codec_id; /* CodecID 含义: * 2: Sorenson H.263 * 3: Screen video * 4: On2 VP6 * 5: On2 VP6 with alpha channel * 6: Screen video version 2 * 7: AVC (H.264) * 12: HEVC (H.265) */ const char* codec_name = "Unknown"; switch (codec_id) { case 2: codec_name = "Sorenson H.263"; break; case 3: codec_name = "Screen video"; break; case 4: codec_name = "On2 VP6"; break; case 5: codec_name = "On2 VP6 (alpha)"; break; case 6: codec_name = "Screen video v2"; break; case 7: codec_name = "AVC (H.264)"; break; case 12: codec_name = "HEVC (H.265)"; break; } printf(" Video Codec: %s (ID: %d)\n", codec_name, codec_id); printf(" Frame Type: %s\n", frame_type == 1 ? "Keyframe" : "Interframe"); /* 如果是 AVC/H.264,可以进一步解析 AVCPacketType */ if (codec_id == 7 && size >= 2) { uint8_t avc_packet_type = data[1]; const char* packet_type_desc = ""; switch (avc_packet_type) { case 0: packet_type_desc = "Sequence Header (SPS/PPS)"; break; case 1: packet_type_desc = "NALU"; break; case 2: packet_type_desc = "Sequence End"; break; } printf(" AVC Packet Type: %s (%d)\n", packet_type_desc, avc_packet_type); } return 0; } /* 解析音频 Tag 数据,获取编码类型和采样率 */ static int parse_audio_tag(const uint8_t* data, uint32_t size, FlvInfo* info) { if (size < 1) return -1; /* SoundFormat (高4位) + SoundRate (2位) + SoundSize (1位) + SoundType (1位) */ uint8_t sound_flags = data[0]; uint8_t sound_format = (sound_flags >> 4) & 0x0F; uint8_t sound_rate = (sound_flags >> 2) & 0x03; uint8_t sound_size = (sound_flags >> 1) & 0x01; uint8_t sound_type = sound_flags & 0x01; info->audio_codec = sound_format; /* SoundFormat 含义: * 0: Linear PCM (platform endian) * 1: ADPCM * 2: MP3 * 3: Linear PCM (little endian) * 4: Nellymoser 16kHz * 5: Nellymoser 8kHz * 6: Nellymoser * 7: G.711 A-law * 8: G.711 mu-law * 9: reserved * 10: AAC * 11: Speex * 14: MP3 8kHz * 15: Device-specific */ const char* codec_name = "Unknown"; switch (sound_format) { case 0: codec_name = "Linear PCM"; break; case 1: codec_name = "ADPCM"; break; case 2: codec_name = "MP3"; break; case 3: codec_name = "Linear PCM (LE)"; break; case 10: codec_name = "AAC"; break; case 11: codec_name = "Speex"; break; } /* 采样率 */ const uint32_t sample_rates[] = {5512, 11025, 22050, 44100}; info->sample_rate = sample_rates[sound_rate]; printf(" Audio Codec: %s (ID: %d)\n", codec_name, sound_format); printf(" Sample Rate: %d Hz\n", info->sample_rate); printf(" Sound Size: %d-bit\n", sound_size ? 16 : 8); printf(" Sound Type: %s\n", sound_type ? "Stereo" : "Mono"); /* 如果是 AAC,可以进一步解析 AACPacketType */ if (sound_format == 10 && size >= 2) { uint8_t aac_packet_type = data[1]; const char* packet_type_desc = aac_packet_type == 0 ? "Sequence Header" : "Raw Data"; printf(" AAC Packet Type: %s (%d)\n", packet_type_desc, aac_packet_type); } return 0; } /* 主解析函数 */ int flv_parse(const char* filename, FlvInfo* info) { FILE* fp = NULL; uint32_t max_timestamp = 0; uint32_t tag_count = 0; if (!filename || !info) { return -1; } /* 初始化 info 结构体 */ memset(info, 0, sizeof(FlvInfo)); strncpy(info->filename, filename, sizeof(info->filename) - 1); /* 获取文件大小 */ info->file_size = get_file_size(filename); if (info->file_size < 0) { fprintf(stderr, "Error: Cannot get file size for %s\n", filename); return -1; } /* 打开文件 */ fp = fopen(filename, "rb"); if (!fp) { fprintf(stderr, "Error: Cannot open file %s\n", filename); return -1; } /* 解析 FLV 头 */ if (parse_flv_header(fp, info) < 0) { fclose(fp); return -1; } printf("\n--- Parsing Tags ---\n"); /* 遍历所有 Tag */ while (1) { uint8_t tag_type; uint32_t data_size; uint32_t timestamp; uint8_t* data = NULL; long tag_start_pos; /* 读取 PreviousTagSize (4 bytes) */ uint32_t prev_tag_size; if (fread(&prev_tag_size, 1, PREV_TAG_SIZE_SIZE, fp) != PREV_TAG_SIZE_SIZE) { break; /* 文件结束 */ } /* 记录 Tag 起始位置 */ tag_start_pos = ftell(fp); /* 解析 Tag 头 */ if (parse_tag_header(fp, &tag_type, &data_size, ×tamp) < 0) { break; } /* 检查数据有效性 */ if (data_size == 0 || data_size > 10 * 1024 * 1024) { /* 限制最大 10MB */ /* 跳过无效数据 */ fseek(fp, data_size, SEEK_CUR); continue; } /* 读取 Tag 数据 */ data = (uint8_t*)malloc(data_size); if (!data) { break; } if (fread(data, 1, data_size, fp) != data_size) { free(data); break; } /* 更新时间戳最大值 */ if (timestamp > max_timestamp) { max_timestamp = timestamp; } /* 根据 Tag 类型处理 */ printf("\nTag #%u:\n", ++tag_count); printf(" Type: %s (0x%02X)\n", tag_type == FLV_TAG_TYPE_AUDIO ? "Audio" : tag_type == FLV_TAG_TYPE_VIDEO ? "Video" : tag_type == FLV_TAG_TYPE_SCRIPT ? "Script" : "Unknown", tag_type); printf(" Data size: %u bytes\n", data_size); printf(" Timestamp: %u ms (%.2f sec)\n", timestamp, timestamp / 1000.0); /* 根据类型解析具体数据 */ if (tag_type == FLV_TAG_TYPE_VIDEO && data_size > 0) { parse_video_tag(data, data_size, info); } else if (tag_type == FLV_TAG_TYPE_AUDIO && data_size > 0) { parse_audio_tag(data, data_size, info); } else if (tag_type == FLV_TAG_TYPE_SCRIPT && data_size > 0) { parse_metadata(data, data_size, info); } free(data); } /* 计算视频时长 (最大时间戳差值) */ if (max_timestamp > 0) { info->duration = max_timestamp / 1000.0; } fclose(fp); return 0; } /* 打印解析结果 */ void print_flv_info(const FlvInfo* info) { printf("\n========== FLV File Information ==========\n"); printf("File: %s\n", info->filename); printf("File size: %ld bytes (%.2f KB, %.2f MB)\n", info->file_size, info->file_size / 1024.0, info->file_size / (1024.0 * 1024.0)); printf("Duration: %.2f seconds\n", info->duration); if (info->duration > 0) { int hours = (int)(info->duration / 3600); int minutes = (int)((info->duration - hours * 3600) / 60); int seconds = (int)(info->duration - hours * 3600 - minutes * 60); printf("Duration (H:M:S): %02d:%02d:%02d\n", hours, minutes, seconds); } printf("Video track: %s\n", info->video_track ? "Yes" : "No"); printf("Audio track: %s\n", info->audio_track ? "Yes" : "No"); if (info->video_track) { printf("Video codec ID: %u\n", info->video_codec); } if (info->audio_track) { printf("Audio codec ID: %u\n", info->audio_codec); printf("Audio sample rate: %u Hz\n", info->sample_rate); } printf("==========================================\n"); } /* 主函数 */ int main(int argc, char* argv[]) { FlvInfo info; int ret; if (argc < 2) { fprintf(stderr, "Usage: %s <flv_file>\n", argv[0]); fprintf(stderr, "Example: %s test.flv\n", argv[0]); return 1; } /* 初始化 libflv */ flv_init(); /* 解析 FLV 文件 */ ret = flv_parse(argv[1], &info); if (ret == 0) { print_flv_info(&info); } else { fprintf(stderr, "Failed to parse FLV file\n"); } /* 清理 libflv */ flv_deinit(); return ret; }📋 程序说明
核心功能
| 功能 | 实现方式 |
|---|---|
| 获取文件大小 | 使用stat()系统调用 |
| 获取视频时长 | 遍历所有 Tag,记录最大时间戳 (毫秒) |
| 解析编解码信息 | 解析 Video/Audio Tag 中的编码标识 |
FLV 文件结构解析
text
FLV 文件结构: ┌─────────────────┐ │ FLV Header │ 9 bytes: "FLV" + version + flags + header_size ├─────────────────┤ │ PreviousTagSize │ 4 bytes: 第一个 Tag 前为 0 ├─────────────────┤ │ Tag #1 │ 11 bytes header + data_size ├─────────────────┤ │ PreviousTagSize │ 4 bytes: Tag #1 大小 ├─────────────────┤ │ Tag #2 │ ├─────────────────┤ │ ... │ └─────────────────┘
Tag 类型说明
| Tag 类型 | 值 | 说明 |
|---|---|---|
| Audio Tag | 0x08 | 音频数据 |
| Video Tag | 0x09 | 视频数据 |
| Script Tag | 0x12 | 脚本/元数据 (onMetaData) |
视频时长计算原理
FLV 文件中每个 Video/Audio Tag 都包含一个时间戳(单位:毫秒),表示该帧相对于文件开始的时间。遍历所有 Tag,取最大时间戳即为视频总时长。
编译与运行
bash
# 编译 gcc -o flv_info flv_info.c -lflv # 运行 ./flv_info test.flv
输出示例
text
FLV Version: 1 Data offset: 9 bytes Video track: Yes Audio track: Yes ========== FLV File Information ========== File: test.flv File size: 1048576 bytes (1024.00 KB, 1.00 MB) Duration: 30.12 seconds Duration (H:M:S): 00:00:30 Video track: Yes Audio track: Yes Video codec ID: 7 Audio codec ID: 10 Audio sample rate: 44100 Hz ==========================================
⚠️ 注意事项
API 适配:本代码假设 libflv 提供
flv_init()、flv_deinit()、flv_open()、flv_read_next_tag()等 API 。如果你的 libflv 版本 API 不同,需要根据实际头文件调整。Metadata 解析:onMetaData 采用 AMF 编码格式,完整解析较为复杂 。本代码提供了简化版本,实际使用建议使用专门的 AMF 解析库。
大文件处理:对于大文件,遍历所有 Tag 可能耗时较长。如需快速获取时长,可以优先解析 onMetaData 中的
duration字段,无需遍历全部 Tag。错误处理:生产环境中应增加更完善的错误处理,如文件损坏、格式异常等情况。
方法二,只读取duration字段
/** * get_flv_duration.c * 功能: 快速从 FLV 文件的 onMetaData 中读取时长,不遍历整个文件 * * 编译: gcc -o get_flv_duration get_flv_duration.c -lm * 运行: ./get_flv_duration test.flv */ #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <math.h> // 从大端序的字节数组中读取双精度浮点数 (IEEE 754) double read_be_double(const uint8_t *data) { uint64_t bits = 0; for (int i = 0; i < 8; i++) { bits = (bits << 8) | data[i]; } double result; memcpy(&result, &bits, sizeof(result)); return result; } int main(int argc, char *argv[]) { if (argc < 2) { fprintf(stderr, "用法: %s <FLV文件路径>\n", argv[0]); return 1; } const char *filename = argv[1]; FILE *fp = fopen(filename, "rb"); if (!fp) { perror("打开文件失败"); return 1; } // 1. 跳过 FLV 文件头 (9 bytes) if (fseek(fp, 9, SEEK_SET) != 0) { fprintf(stderr, "错误: 文件太小,不是有效的FLV文件\n"); fclose(fp); return 1; } // 2. 跳过第一个 PreviousTagSize 字段 (4 bytes) if (fseek(fp, 4, SEEK_CUR) != 0) { fprintf(stderr, "错误: 无法跳过 PreviousTagSize\n"); fclose(fp); return 1; } // 3. 读取第一个 Tag 的头部 uint8_t tag_header[11]; if (fread(tag_header, 1, 11, fp) != 11) { fprintf(stderr, "错误: 无法读取第一个 Tag 的头部\n"); fclose(fp); return 1; } // 检查 Tag 类型: 0x12 代表 Script Tag (onMetaData) if (tag_header[0] != 0x12) { fprintf(stderr, "信息: 第一个 Tag 不是 onMetaData (类型: 0x%02X)\n", tag_header[0]); fclose(fp); return 1; } // 4. 解析 Tag 数据部分的大小 (24-bit big-endian) uint32_t data_size = (tag_header[1] << 16) | (tag_header[2] << 8) | tag_header[3]; if (data_size == 0 || data_size > 1024 * 1024) { // 做个安全检查 fprintf(stderr, "错误: onMetaData 数据大小异常 (%u bytes)\n", data_size); fclose(fp); return 1; } // 5. 读取 onMetaData 的二进制数据 uint8_t *data = (uint8_t *)malloc(data_size); if (!data) { fprintf(stderr, "内存分配失败\n"); fclose(fp); return 1; } if (fread(data, 1, data_size, fp) != data_size) { fprintf(stderr, "读取 onMetaData 数据失败\n"); free(data); fclose(fp); return 1; } // 6. 在二进制数据中解析 AMF0 格式,查找 duration 字段 // 数据格式: [String: "onMetaData"] [ECMA Array] ... int offset = 0; // 第一个 AMF 项应该是 String (0x02) if (offset + 1 > data_size || data[offset] != 0x02) { fprintf(stderr, "错误: 未找到 onMetaData 字符串标记\n"); free(data); fclose(fp); return 1; } offset += 1; // 跳过类型标记 // 读取字符串长度 (2 bytes, big-endian) uint16_t str_len = (data[offset] << 8) | data[offset + 1]; offset += 2; if (str_len != 10 || memcmp(&data[offset], "onMetaData", 10) != 0) { fprintf(stderr, "错误: 未找到 onMetaData 字符串\n"); free(data); fclose(fp); return 1; } offset += 10; // 跳过字符串内容 // 第二个 AMF 项应该是 ECMA Array (0x08) if (offset + 1 > data_size || data[offset] != 0x08) { fprintf(stderr, "错误: 未找到 ECMA Array 标记\n"); free(data); fclose(fp); return 1; } offset += 1; // 跳过类型标记 // 跳过数组元素个数 (4 bytes) offset += 4; // 遍历数组元素,查找 "duration" double duration = -1.0; while (offset + 3 < data_size) { // 读取元素名称的长度 (2 bytes, big-endian) uint16_t key_len = (data[offset] << 8) | data[offset + 1]; offset += 2; if (offset + key_len + 1 > data_size) { break; // 数据不完整,退出循环 } // 检查是否是 "duration" if (key_len == 8 && memcmp(&data[offset], "duration", 8) == 0) { offset += key_len; // 跳过键名 // 下一个字节是值的类型,应该是 DOUBLE (0x00) if (offset + 1 > data_size || data[offset] != 0x00) { fprintf(stderr, "警告: duration 的值类型不是 DOUBLE\n"); break; } offset += 1; // 跳过类型标记 // 读取 8 字节的 DOUBLE 值 if (offset + 8 > data_size) { fprintf(stderr, "错误: duration 数据不完整\n"); break; } duration = read_be_double(&data[offset]); break; // 找到 duration,退出循环 } else { // 不是 duration,跳过这个键值对 offset += key_len; // 跳过键名 if (offset + 1 > data_size) break; uint8_t val_type = data[offset]; offset += 1; // 跳过值的类型标记 // 根据不同类型,跳过对应大小的值 switch (val_type) { case 0x00: // DOUBLE offset += 8; break; case 0x01: // BOOLEAN offset += 1; break; case 0x02: // STRING if (offset + 2 > data_size) break; uint16_t val_len = (data[offset] << 8) | data[offset + 1]; offset += 2 + val_len; break; // 可以添加更多类型的处理,但为了简洁,其他类型简单跳过 default: // 对于复杂类型,简单跳过是不安全的,但对于查找 duration 足够了 // 这里为了安全,直接退出循环 fprintf(stderr, "警告: 遇到未处理的数据类型 0x%02X,停止解析\n", val_type); offset = data_size; // 退出循环 break; } } } // 7. 输出结果 if (duration > 0) { printf("文件: %s\n", filename); printf("视频时长: %.3f 秒\n", duration); // 也可以输出时、分、秒格式 int hours = (int)(duration / 3600); int minutes = (int)((duration - hours * 3600) / 60); int seconds = (int)(duration - hours * 3600 - minutes * 60); printf(" (H:M:S): %02d:%02d:%02d\n", hours, minutes, seconds); } else { fprintf(stderr, "错误: 未能在 onMetaData 中找到 duration 字段\n"); } free(data); fclose(fp); return 0; }⚠️ 注意事项
非所有 FLV 都有 onMetaData:虽然这是主流编码器(如FFmpeg)的标准做法,但某些 FLV 文件可能不包含这个 Tag。如果代码执行后未找到
duration,则只能退回到之前的遍历方案。duration 的精确性:Adobe 官方文档曾指出,
onMetaData中的duration是一个近似值,但通常与真实时长的误差非常小,完全可以满足常规需求。字节序问题:FLV 和 AMF 格式均采用网络字节序(大端序,Big-Endian),而你的嵌入式设备可能是小端序(Little-Endian)。代码中的
read_be_double函数就是为了处理这个转换。