FLV写入文件遇到的坑

音视频写入之后能播放,但是播放器总是跳着播放,一开始就蹦到第5秒第6秒的样子

经过一番分析,无论是视频还是音频都会有先有后,因为数据源不是同出一处,暂时没有好的办法控制它,只能想办法将音频的数据强制卡在视频之后写入,但是问题并没有修复,因为时间戳差距很大无论怎么调整写入的顺序都是徒劳,还是会跳着播放~
问了AI,大概意思是不同的播放器的解码策略不同,有的会以视频的第一个关键帧为基准,而有些播放器则按音频时间戳为准,无论以哪一个为准,第一个视频帧必须为关键帧,因为flv需要读取解码配置如果不是关键帧则会直接黑屏... Infuse和IINA播放器都是如此,解码失败提示流不规范之类的

问题本质

  • 有的播放器以音频首帧时间戳为起点,有的以视频关键帧为起点。

  • 如果音频/视频的首帧时间戳不对齐,或者有一方首帧时间戳不是0,进度条就会跳。

  • 有的播放器遇到音频首帧时间戳大于视频,会直接seek到音频首帧。

  • 有的播放器遇到视频首帧时间戳大于音频,会直接seek到视频首帧。

可能出现的问题

时间戳起点不一致
  • 如果音频和视频的首帧timestamp不是同一个(比如音频先采到,视频关键帧晚到),就会出现“进度条跳到N秒”的现象。
音视频不同步
  • 如果音频和视频的首帧timestamp差距大,播放器会以较大的那一方为起点,导致前面一段音/视频丢失。
首包策略不统一
  • 有的播放器要求FLV文件第一个Tag必须是音频或视频关键帧,否则会花屏或无声。

优化建议

统一音视频时间戳起点

  • 采集到的第一个音频帧和第一个视频关键帧的timestamp,取最小值,作为“起点”,后续所有音视频帧的timestamp都减去这个起点。

  • 这样FLV文件的音视频首帧timestamp都是0,播放器不会跳

音频和视频首帧都要写入

  • 建议无论音频还是视频,首帧都要写入,不要等到视频关键帧才写音频。

  • 可以缓存音频帧,等到收到第一个视频关键帧后,把缓存的音频帧一起写出(时间戳都减去baseTimestamp)

  • FLV的MetaData、Header等Tag时间戳都应为0。

以上这样应该可以最大程度兼容各种播放器,避免进度条跳动、音视频不同步等问题

补充:

  1. 如果视频帧首帧不是关键帧,可以丢弃掉,等到第一个关键帧再开始写入,这样可以避免很多问题,一般来说编码关键帧间隔只要不是设置很大基本上就前面几帧的样子

遇到最后一个tag结构不完整的情况进行修复

#import <Foundation/Foundation.h>

@interface FLVMetadataEditor : NSObject

///生成MetaData 如果需要回写 录制初始写入时就应该把字段定好 不然结构会有问题
+ (NSData *)generateAMFMetadata:(NSDictionary<NSString *, id> *)metadata;

///读取MetaData信息
+ (NSDictionary<NSString *, id> *)extractMetadataFromFLVFile:(NSString *)filePath ;

///修复文件 把不完整的tag剔除掉 其实这种问题应该在写入时先判断不足一个tag 就别写入最好 但是这种问题可能发生在写入过程中也不好说 这也是一种后期修复的手段
+ (void)repairIfNeededWithFilePath:(NSString *)filePath;

@end

#import <UIKit/UIKit.h>
#import "FLVMetadataEditor.h"

// FLV文件结构常量
static const NSUInteger FLV_HEADER_SIZE = 9;
static const NSUInteger TAG_HEADER_SIZE = 11;
static const NSUInteger PREV_TAG_SIZE = 4;

@implementation FLVMetadataEditor


+ (NSDictionary<NSString *, id> *)extractMetadataFromFLVFile:(NSString *)filePath {
    NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:filePath];
    if (!fileHandle) return nil;
    
    // 读取FLV Header
    [fileHandle seekToFileOffset:0];
    NSData *headerData = [fileHandle readDataOfLength:FLV_HEADER_SIZE];
    if (headerData.length < FLV_HEADER_SIZE) {
        [fileHandle closeFile];
        return nil;
    }
    
    // 检查FLV签名
    const char *headerBytes = (const char *)[headerData bytes];
    if (strncmp(headerBytes, "FLV", 3) != 0) {
        [fileHandle closeFile];
        return nil; // 不是有效的FLV文件
    }
    
    // 跳过第一个PreviousTagSize
    [fileHandle seekToFileOffset:FLV_HEADER_SIZE + PREV_TAG_SIZE];
    
    NSMutableDictionary *metadata = [NSMutableDictionary dictionary];
    BOOL foundMetadata = NO;
    
    // 遍历前5个Tag(通常Metadata在文件开头)
    for (int i = 0; i < 5; i++) {
        // 读取Tag Header
        NSData *tagHeader = [fileHandle readDataOfLength:TAG_HEADER_SIZE];
        if (tagHeader.length < TAG_HEADER_SIZE) break;
        
        const uint8_t *tagBytes = (const uint8_t *)[tagHeader bytes];
        uint8_t tagType = tagBytes[0];
        
        // 计算DataSize
        uint32_t dataSize = (tagBytes[1] << 16) | (tagBytes[2] << 8) | tagBytes[3];
        
        // 读取Tag数据
        NSData *tagData = [fileHandle readDataOfLength:dataSize];
        if (tagData.length != dataSize) break;
        
        // 跳过PreviousTagSize
        [fileHandle seekToFileOffset:[fileHandle offsetInFile] + PREV_TAG_SIZE];
        
        if (tagType == 18) { // Script Tag
            NSDictionary *scriptDict = [self parseAMFMetadata:tagData];
            if (scriptDict) {
                [metadata addEntriesFromDictionary:scriptDict];
                foundMetadata = YES;
                break; // 找到Metadata后停止搜索
            }
        }
    }
    
    [fileHandle closeFile];
    return foundMetadata ? metadata : nil;
}

 
#pragma mark - AMF Processing

+ (NSData *)generateAMFMetadata:(NSDictionary<NSString *, id> *)metadata {
    NSMutableData *data = [NSMutableData data];
    
    // 1. 字符串 "onMetaData" (必需标识)
    const char *metaDataStr = "onMetaData";
    uint16_t strLen = (uint16_t)strlen(metaDataStr);
    
    // AMF0 字符串类型标记 (0x02)
    [data appendBytes:"\x02" length:1];
    
    // 大端序字符串长度
    uint16_t beStrLen = CFSwapInt16HostToBig(strLen);
    [data appendBytes:&beStrLen length:2];
    
    // 字符串内容
    [data appendBytes:metaDataStr length:strLen];
    
    // 2. ECMA 数组类型标记 (0x08)
    [data appendBytes:"\x08" length:1];
    
    // 数组元素数量 (大端序)
    uint32_t count = (uint32_t)metadata.count;
    uint32_t beCount = CFSwapInt32HostToBig(count);
    [data appendBytes:&beCount length:4];
    
    // 3. 添加键值对
    for (NSString *key in metadata) {
        id value = metadata[key];
        
        // Key (字符串)
        const char *cKey = [key UTF8String];
        uint16_t keyLen = (uint16_t)strlen(cKey);
        
        // 大端序键长度
        uint16_t beKeyLen = CFSwapInt16HostToBig(keyLen);
        [data appendBytes:&beKeyLen length:2];
        [data appendBytes:cKey length:keyLen];
        
        // Value 类型处理
        if ([value isKindOfClass:[NSNumber class]]) {
            // 布尔值特殊处理
            if (strcmp([value objCType], @encode(BOOL)) == 0) {
                [data appendBytes:"\x01" length:1]; // AMF0 布尔类型
                uint8_t boolValue = [value boolValue] ? 1 : 0;
                [data appendBytes:&boolValue length:1];
            }
            // 数字类型
            else {
                [data appendBytes:"\x00" length:1]; // AMF0 数字类型
                double doubleValue = [value doubleValue];
                uint64_t beDoubleValue = CFSwapInt64HostToBig(*(uint64_t *)&doubleValue);
                [data appendBytes:&beDoubleValue length:8];
            }
        }
        // 字符串类型
        else if ([value isKindOfClass:[NSString class]]) {
            [data appendBytes:"\x02" length:1]; // AMF0 字符串类型
            const char *cValue = [value UTF8String];
            uint16_t valueLen = (uint16_t)strlen(cValue);
            uint16_t beValueLen = CFSwapInt16HostToBig(valueLen);
            [data appendBytes:&beValueLen length:2];
            [data appendBytes:cValue length:valueLen];
        }
        // 其他类型转为字符串
        else {
            [data appendBytes:"\x02" length:1];
            NSString *strValue = [value description];
            const char *cValue = [strValue UTF8String];
            uint16_t valueLen = (uint16_t)strlen(cValue);
            uint16_t beValueLen = CFSwapInt16HostToBig(valueLen);
            [data appendBytes:&beValueLen length:2];
            [data appendBytes:cValue length:valueLen];
        }
    }
    
    // 4. 数组结束标记 (0x000009)
    [data appendBytes:"\x00\x00\x09" length:3];
    
    return data;
}


+ (NSDictionary *)parseAMFMetadata:(NSData *)data {
    if (data.length < 15) return nil; // 最小长度检查
    
    NSMutableDictionary *result = [NSMutableDictionary dictionary];
    const uint8_t *bytes = (const uint8_t *)[data bytes];
    NSUInteger position = 0;
    NSUInteger length = data.length;
    
    // 1. 检查字符串 "onMetaData"
    if (position + 3 > length || bytes[position++] != 0x02) return nil;
    
    uint16_t strLen = (bytes[position] << 8) | bytes[position + 1];
    position += 2;
    
    if (position + strLen > length || strncmp((const char *)(bytes + position), "onMetaData", strLen) != 0) {
        return nil;
    }
    position += strLen;
    
    // 2. 检查ECMA数组类型
    if (position >= length || bytes[position++] != 0x08) return nil;
    
    // 3. 读取元素数量 (4字节大端序)
    if (position + 4 > length) return nil;
    uint32_t count = (bytes[position] << 24) |
                    (bytes[position+1] << 16) |
                    (bytes[position+2] << 8) |
                    bytes[position+3];
    position += 4;
    
    // 4. 解析键值对
    for (uint32_t i = 0; i < count && position < length; i++) {
        // 读取键长度
        if (position + 2 > length) break;
        uint16_t keyLen = (bytes[position] << 8) | bytes[position+1];
        position += 2;
        
        // 读取键
        if (position + keyLen > length) break;
        NSString *key = [[NSString alloc] initWithBytes:bytes + position
                                                length:keyLen
                                              encoding:NSUTF8StringEncoding];
        position += keyLen;
        
        // 读取值类型
        if (position >= length) break;
        uint8_t valueType = bytes[position++];
        
        id value = nil;
        
        switch (valueType) {
            case 0x00: // 数字
                if (position + 8 > length) break;
                {
                    uint64_t valueBytes = 0;
                    for (int j = 0; j < 8; j++) {
                        valueBytes = (valueBytes << 8) | bytes[position++];
                    }
                    double doubleValue;
                    memcpy(&doubleValue, &valueBytes, sizeof(double));
                    value = @(doubleValue);
                }
                break;
                
            case 0x01: // 布尔
                if (position >= length) break;
                value = @(bytes[position++] != 0);
                break;
                
            case 0x02: { // 字符串
                if (position + 2 > length) break;
                uint16_t valueLen = (bytes[position] << 8) | bytes[position+1];
                position += 2;
                
                if (position + valueLen > length) break;
                value = [[NSString alloc] initWithBytes:bytes + position
                                                length:valueLen
                                              encoding:NSUTF8StringEncoding];
                position += valueLen;
                break;
            }
                
            default:
                // 跳过未知类型
                position += 8;
                break;
        }
        
        if (key && value) {
            result[key] = value;
        }
    }
    
    return result.count > 0 ? result : nil;
}


// 修复可能损坏的末尾Tag
+ (void)repairIfNeededWithFilePath:(NSString *)filePath {
    NSLog(@"%@",filePath);
    NSFileHandle *repairHandle = [NSFileHandle fileHandleForUpdatingAtPath:filePath];
    if (!repairHandle){
        NSLog(@"repairHandle创建失败!!!");
        return;
    }
    
    @try {
        unsigned long long fileSize = [repairHandle seekToEndOfFile];
        if (fileSize < 13) { // FLV头部最小13字节
            [repairHandle truncateFileAtOffset:0];
            NSLog(@"文件已损坏,重置为空文件");
            return;
        }
        
        // 检查末尾PreviousTagSize是否完整(最后4字节)
        if (fileSize >= 4) {
            [repairHandle seekToFileOffset:fileSize - 4];
            NSData *lastBytes = [repairHandle readDataOfLength:4];
            
            if (lastBytes.length == 4) {
                uint32_t lastTagSize = CFSwapInt32BigToHost(*(uint32_t *)lastBytes.bytes);
                unsigned long long tagStart = fileSize - 4 - lastTagSize;
                
                // 验证Tag头部完整性 (最小11字节)
                if (tagStart >= 13 && tagStart + 11 <= fileSize - 4) {
                    [repairHandle seekToFileOffset:tagStart];
                    NSData *tagHeader = [repairHandle readDataOfLength:11];
                    
                    if (tagHeader.length == 11) {
                        const uint8_t *bytes = tagHeader.bytes;
                        uint32_t dataSize = (bytes[3] << 16) | (bytes[4] << 8) | bytes[5];
                        
                        // 验证Tag总大小:头部11 + 数据体 + 尾部4
                        if (tagStart + 11 + dataSize + 4 == fileSize) {
                            NSLog(@"FLV文件完整,无需修复");
                            return;
                        }
                    }
                }
            }
            
            // 如果不完整,截断到最后一个完整Tag结束位置
            [repairHandle truncateFileAtOffset:fileSize - 4];
            NSLog(@"已修复FLV文件,移除末尾不完整Tag");
        }
    } @catch (NSException *exception) {
        NSLog(@"修复FLV文件失败: %@", exception);
    } @finally {
        [repairHandle closeFile];
    }
}

@end

posted @ 2025-07-04 22:08  CoderWGB  阅读(30)  评论(0)    收藏  举报