// 定义分析结果的类型 interface AnalysisResult { found: boolean offset: number | null elapsedTime: number | null error: string | null } // 异步函数用于分析 FLV 视频 export async function analyzeFLV( videoUrl: string, timeoutSeconds = 10, ): Promise<AnalysisResult> { const startTime = new Date() console.log(`开始分析时间: ${startTime.toLocaleTimeString()}`) console.log('开始请求实时流') const timeoutMs = timeoutSeconds * 1000 let result: AnalysisResult = { found: false, offset: null, elapsedTime: null, error: null, } try { const response = await fetch(videoUrl) if (!response.ok) { throw new Error(`请求失败: ${response.status}`) } const reader = response.body!.getReader() let buffer = new Uint8Array() let iFrameFound = false let iFrameOffset: number | null = null let iFrameTime: Date | null = null const timeoutId = setTimeout(() => { if (!iFrameFound) { const elapsedTime = (new Date().getTime() - startTime.getTime()) / 1000 console.log(`经过 ${elapsedTime.toFixed(2)} 秒,未检测到 I 帧`) reader.cancel() } }, timeoutMs) while (!iFrameFound) { const { value, done } = await reader.read() if (done) { break } buffer = concatUint8Arrays(buffer, value!) const analysisResult = checkFirstFrame(buffer, startTime) if (analysisResult.found) { iFrameFound = true iFrameOffset = analysisResult.offset iFrameTime = new Date() } } clearTimeout(timeoutId) if (iFrameFound) { const elapsedTime = (iFrameTime!.getTime() - startTime.getTime()) / 1000 console.log( `经过 ${elapsedTime.toFixed(2)} 秒,在位置 ${iFrameOffset} 检测到 I 帧`, ) result = { found: true, offset: iFrameOffset, elapsedTime, error: null, } } } catch (err) { const error = err as Error console.error(`错误: ${error.message}`) result = { found: false, offset: null, elapsedTime: null, error: error.message, } } return result } // 合并两个 Uint8Array function concatUint8Arrays(a: Uint8Array, b: Uint8Array): Uint8Array { const result = new Uint8Array(a.length + b.length) result.set(a, 0) result.set(b, a.length) return result } // 检查是否找到第一个 I 帧 function checkFirstFrame( buffer: Uint8Array, startTime: Date, ): { found: boolean; offset: number | null } { const dataView = new DataView(buffer.buffer) const uint8 = new Uint8Array(buffer.buffer) console.log('开始验证 FLV 文件头') // 1. 验证FLV文件头 if ( uint8.length < 3 || uint8[0] !== 0x46 || uint8[1] !== 0x4C || uint8[2] !== 0x56 ) { console.log('数据不足或不是有效的 FLV 文件') return { found: false, offset: null } } console.log('FLV 文件头验证通过') console.log('开始检查是否包含视频流') // 2. 检查是否包含视频流 if (uint8.length < 5) { console.log('数据不足,无法检查视频流') return { found: false, offset: null } } const hasVideo = (uint8[4] & 0x01) !== 0 if (!hasVideo) { console.log('文件不包含视频流') return { found: false, offset: null } } console.log('文件包含视频流') // 3. 解析Tags let offset = 13 // FLV头(9) + 第一个PreviousTagSize(4) console.log(`开始解析 Tags,初始偏移量: ${offset}`) while (offset + 11 < buffer.length) { const tagType = dataView.getUint8(offset) const dataSize = (dataView.getUint8(offset + 1) << 16) | (dataView.getUint8(offset + 2) << 8) | dataView.getUint8(offset + 3) console.log(`当前 Tag 类型: ${tagType},数据大小: ${dataSize}`) // 遇到视频Tag时处理 if (tagType === 9) { const payloadOffset = offset + 11 if (payloadOffset >= buffer.length) { console.log('数据不足,无法解析视频帧') return { found: false, offset: null } } console.log(`找到视频 Tag,有效负载偏移量: ${payloadOffset}`) // 获取帧类型(前4位) const frameInfo = dataView.getUint8(payloadOffset) const frameType = frameInfo >> 4 if (frameType === 1) { return { found: true, offset: payloadOffset } } } // 移动到下一个Tag offset += 11 + dataSize + 4 // 当前Tag头+数据+下一个PreviousTagSize console.log(`移动到下一个 Tag,新的偏移量: ${offset}`) } console.log('未找到视频帧,继续等待数据') return { found: false, offset: null } } // 示例调用 // const videoUrl = 'your_flv_video_url'; // analyzeFLV(videoUrl, 15).then((result) => { // console.log('分析结果:', result); // });