// 定义日志函数,将日志信息打印到控制台 function logMessage(message: string): void { console.log(`${new Date().toLocaleTimeString()}: ${message}`) } // 拼接两个 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 } // 检查 FLV 文件的首帧是否为关键帧 async function checkFirstFrame( buffer: Uint8Array, startTime: Date, ): Promise<boolean | string | null> { const dataView = new DataView(buffer.buffer) const uint8 = new Uint8Array(buffer.buffer) logMessage('开始验证 FLV 文件头') // 1. 验证 FLV 文件头 if ( uint8.length < 3 || uint8[0] !== 0x46 || uint8[1] !== 0x4C || uint8[2] !== 0x56 ) { logMessage('数据不足或不是有效的 FLV 文件') return null } logMessage('FLV 文件头验证通过') logMessage('开始检查是否包含视频流') // 2. 检查是否包含视频流 if (uint8.length < 5) { logMessage('数据不足,无法检查视频流') return null } const hasVideo = (uint8[4] & 0x01) !== 0 if (!hasVideo) { logMessage('文件不包含视频流') return '文件不包含视频流' } logMessage('文件包含视频流') // 3. 解析 Tags let offset = 13 // FLV 头(9) + 第一个 PreviousTagSize(4) logMessage(`开始解析 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) logMessage(`当前 Tag 类型: ${tagType},数据大小: ${dataSize}`) // 遇到视频 Tag 时处理 if (tagType === 9) { const payloadOffset = offset + 11 if (payloadOffset >= buffer.length) { logMessage('数据不足,无法解析视频帧') return null } logMessage(`找到视频 Tag,有效负载偏移量: ${payloadOffset}`) // 获取帧类型(前 4 位) const frameInfo = dataView.getUint8(payloadOffset) const frameType = frameInfo >> 4 const iFrameTime = new Date() logMessage(`获取到 I 帧时间: ${iFrameTime.toLocaleTimeString()}`) return frameType === 1 // 1 表示关键帧 } // 移动到下一个 Tag offset += 11 + dataSize + 4 // 当前 Tag 头 + 数据 + 下一个 PreviousTagSize logMessage(`移动到下一个 Tag,新的偏移量: ${offset}`) } logMessage('未找到视频帧,继续等待数据') return null } // 主分析函数,接收视频 URL 作为参数 export async function analyzeFLVFirstFrame( videoUrl: string, ): Promise<boolean | string | null> { const startTime = new Date() logMessage(`开始分析时间: ${startTime.toLocaleTimeString()}`) try { logMessage('开始请求实时流') const response = await fetch(videoUrl) if (!response.ok) { throw new Error(`请求失败: ${response.status}`) } const reader = response.body?.getReader() if (!reader) { throw new Error('无法获取响应体的读取器') } let buffer = new Uint8Array() let foundIFrame = false let result: boolean | string | null = null while (!foundIFrame) { const { value, done } = await reader.read() if (done) { break } buffer = concatUint8Arrays(buffer, value) result = await checkFirstFrame(buffer, startTime) if (typeof result === 'boolean') { foundIFrame = true } } return result } catch (err) { logMessage(`错误: ${(err as Error).message}`) console.error(err) return `错误: ${(err as Error).message}` } }