以下是利用 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函数就是为了处理这个转换。