Newer
Older
safe_production_front / src / utils / firstIframeTest.ts
// 定义日志函数,将日志信息打印到控制台
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}`
  }
}