useVideoPlayer.test.js 9.23 KB
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
import { ref } from 'vue'

vi.mock('@/utils/tools', () => ({
  wxInfo: () => ({
    isIOSWeChat: false
  })
}))

vi.mock('hls.js', () => ({
  default: {
    isSupported: () => false
  }
}))

vi.mock('video.js', () => ({
  default: {
    browser: {
      IS_SAFARI: false
    }
  }
}))

vi.mock('videojs-contrib-quality-levels', () => ({}))
vi.mock('videojs-hls-quality-selector', () => ({}))
vi.mock('videojs-hls-quality-selector/dist/videojs-hls-quality-selector.css', () => ({}))

import { useVideoPlayer } from '../useVideoPlayer'

const createPlayerStub = ({ bandwidthBitsPerSecond, bandwidthOn = 'vhs' } = {}) => {
  const listeners = {}
  const techObject = bandwidthOn === 'hls'
    ? { hls: { bandwidth: bandwidthBitsPerSecond } }
    : { vhs: { bandwidth: bandwidthBitsPerSecond } }

  const player = {
    isDisposed: () => false,
    on: (eventName, callback) => {
      listeners[eventName] = callback
    },
    tech: () => techObject,
    hlsQualitySelector: vi.fn(),
    qualityLevels: vi.fn(() => ({ on: vi.fn() })),
    error: () => null,
    load: vi.fn(),
    pause: vi.fn(),
    play: vi.fn(() => Promise.resolve())
  }

  return { player, listeners }
}

describe('useVideoPlayer HLS 下载速度调试', () => {
  let console_warn_spy

  beforeEach(() => {
    console_warn_spy = vi.spyOn(console, 'warn').mockImplementation(() => {})
    vi.useFakeTimers()
  })

  afterEach(() => {
    vi.clearAllTimers()
    vi.useRealTimers()
    console_warn_spy?.mockRestore?.()
  })

  it('m3u8 + 非原生模式下能从 vhs.bandwidth 计算速度', () => {
    const props = {
      options: {},
      videoUrl: 'https://example.com/test.m3u8',
      videoId: 'v1',
      autoplay: false,
      debug: true,
      useNativeOnIos: true
    }

    const emit = vi.fn()
    const videoRef = ref(null)
    const nativeVideoRef = ref(null)

    const { handleVideoJsMounted, hlsDownloadSpeedText, hlsSpeedDebugText } = useVideoPlayer(
      props,
      emit,
      videoRef,
      nativeVideoRef
    )

    const { player, listeners } = createPlayerStub({ bandwidthBitsPerSecond: 8 * 1024 * 1024, bandwidthOn: 'vhs' })

    handleVideoJsMounted({ state: {}, player })
    listeners.play()

    vi.advanceTimersByTime(1100)

    expect(hlsDownloadSpeedText.value).toBe('1.0MB/s')
    expect(hlsSpeedDebugText.value).toContain('update')
    expect(hlsSpeedDebugText.value).toContain('mode:videojs')
    expect(hlsSpeedDebugText.value).toContain('m3u8:1')
  })

  it('m3u8 + 非原生模式下能从 hls.bandwidth 兜底计算速度', () => {
    const props = {
      options: {},
      videoUrl: 'https://example.com/test.m3u8',
      videoId: 'v1',
      autoplay: false,
      debug: true,
      useNativeOnIos: true
    }

    const emit = vi.fn()
    const videoRef = ref(null)
    const nativeVideoRef = ref(null)

    const { handleVideoJsMounted, hlsDownloadSpeedText, hlsSpeedDebugText } = useVideoPlayer(
      props,
      emit,
      videoRef,
      nativeVideoRef
    )

    const { player, listeners } = createPlayerStub({ bandwidthBitsPerSecond: 4 * 1024 * 1024, bandwidthOn: 'hls' })

    handleVideoJsMounted({ state: {}, player })
    listeners.play()

    expect(hlsDownloadSpeedText.value).toBe('512kB/s')
    expect(hlsSpeedDebugText.value).toContain('hls_bw:')
  })

  it('playing 事件的 debug 文本包含 tech 与模式信息', () => {
    const props = {
      options: {},
      videoUrl: 'https://example.com/test.m3u8',
      videoId: 'v1',
      autoplay: false,
      debug: true,
      useNativeOnIos: true
    }

    const emit = vi.fn()
    const videoRef = ref(null)
    const nativeVideoRef = ref(null)

    const { handleVideoJsMounted, hlsSpeedDebugText } = useVideoPlayer(
      props,
      emit,
      videoRef,
      nativeVideoRef
    )

    const { player, listeners } = createPlayerStub({ bandwidthBitsPerSecond: 8 * 1024 * 1024, bandwidthOn: 'vhs' })

    handleVideoJsMounted({ state: {}, player })
    listeners.playing()

    expect(hlsSpeedDebugText.value).toContain('playing')
    expect(hlsSpeedDebugText.value).toContain('mode:videojs')
    expect(hlsSpeedDebugText.value).toContain('tech:')
  })
})

describe('useVideoPlayer 清晰度插件兼容', () => {
  let console_warn_spy

  beforeEach(() => {
    console_warn_spy = vi.spyOn(console, 'warn').mockImplementation(() => {})
  })

  afterEach(() => {
    console_warn_spy?.mockRestore?.()
  })

  it('非 m3u8 源不会初始化清晰度插件', () => {
    const props = {
      options: {},
      videoUrl: 'https://example.com/test.mp4',
      videoId: 'v1',
      autoplay: false,
      debug: false,
      useNativeOnIos: true
    }

    const emit = vi.fn()
    const videoRef = ref(null)
    const nativeVideoRef = ref(null)

    const { handleVideoJsMounted } = useVideoPlayer(
      props,
      emit,
      videoRef,
      nativeVideoRef
    )

    const { player } = createPlayerStub({ bandwidthBitsPerSecond: 8 * 1024 * 1024, bandwidthOn: 'vhs' })

    handleVideoJsMounted({ state: {}, player })

    expect(player.hlsQualitySelector).not.toHaveBeenCalled()
  })

  it('m3u8 + vhs tech 下会别名 hls 并初始化清晰度插件', () => {
    const props = {
      options: {},
      videoUrl: 'https://example.com/test.m3u8',
      videoId: 'v1',
      autoplay: false,
      debug: false,
      useNativeOnIos: true
    }

    const emit = vi.fn()
    const videoRef = ref(null)
    const nativeVideoRef = ref(null)

    const { handleVideoJsMounted } = useVideoPlayer(
      props,
      emit,
      videoRef,
      nativeVideoRef
    )

    const { player } = createPlayerStub({ bandwidthBitsPerSecond: 8 * 1024 * 1024, bandwidthOn: 'vhs' })

    expect(player.tech().hls).toBeUndefined()
    expect(player.tech().vhs).toBeTruthy()

    handleVideoJsMounted({ state: {}, player })

    expect(player.tech().hls).toBe(player.tech().vhs)
    expect(player.hlsQualitySelector).toHaveBeenCalledTimes(1)
  })
})

describe('useVideoPlayer blob URL 兜底类型识别', () => {
  let console_warn_spy

  beforeEach(() => {
    console_warn_spy = vi.spyOn(console, 'warn').mockImplementation(() => {})
  })

  afterEach(() => {
    console_warn_spy?.mockRestore?.()
  })

  it('blob: URL 时可从 videoId 的扩展名推断 type', () => {
    const props = {
      options: {},
      videoUrl: 'blob:http://localhost:8206/41e9655f-37ee-4e48-bfd2-29104d64d7b3',
      videoId: '368_1726304830.mp4',
      autoplay: false,
      debug: false,
      useNativeOnIos: true
    }

    const emit = vi.fn()
    const videoRef = ref(null)
    const nativeVideoRef = ref(null)

    const { videoOptions } = useVideoPlayer(
      props,
      emit,
      videoRef,
      nativeVideoRef
    )

    expect(videoOptions.value.sources[0].type).toBe('video/mp4')
  })
})

describe('useVideoPlayer 重试上限与错误回退', () => {
  beforeEach(() => {
    vi.useFakeTimers()
  })

  afterEach(() => {
    vi.clearAllTimers()
    vi.useRealTimers()
  })

  it('重试失败时恢复错误提示,并在达到上限后不可再重试', () => {
    const props = {
      options: {},
      videoUrl: 'https://example.com/test.mp4',
      videoId: 'v1.mp4',
      autoplay: false,
      debug: false,
      useNativeOnIos: true
    }

    const emit = vi.fn()
    const videoRef = ref(null)
    const nativeVideoRef = ref(null)

    const {
      handleVideoJsMounted,
      retryLoad,
      showErrorOverlay,
      errorMessage,
      canRetry,
      retryCount
    } = useVideoPlayer(
      props,
      emit,
      videoRef,
      nativeVideoRef
    )

    const listeners = {}
    let current_error = { code: 4, message: 'No compatible source was found for this media.' }

    const player = {
      isDisposed: () => false,
      on: (eventName, callback) => {
        listeners[eventName] = callback
      },
      tech: () => ({}),
      hlsQualitySelector: vi.fn(),
      qualityLevels: vi.fn(() => ({ on: vi.fn() })),
      error: vi.fn(function (value) {
        if (arguments.length) {
          current_error = value
          return null
        }
        return current_error
      }),
      src: vi.fn(),
      load: vi.fn(),
      pause: vi.fn(),
      play: vi.fn(() => Promise.resolve())
    }

    handleVideoJsMounted({ state: {}, player })

    listeners.error()
    expect(showErrorOverlay.value).toBe(true)
    expect(errorMessage.value).toBe('视频格式不支持或无法加载,请检查网络连接')
    expect(canRetry.value).toBe(true)

    retryLoad()
    current_error = { code: 4, message: 'No compatible source was found for this media.' }
    vi.advanceTimersByTime(900)
    expect(showErrorOverlay.value).toBe(true)
    expect(errorMessage.value).toBe('视频格式不支持或无法加载,请检查网络连接')
    expect(retryCount.value).toBe(1)
    expect(canRetry.value).toBe(true)

    retryLoad()
    current_error = { code: 4, message: 'No compatible source was found for this media.' }
    vi.advanceTimersByTime(900)
    expect(showErrorOverlay.value).toBe(true)
    expect(retryCount.value).toBe(2)
    expect(canRetry.value).toBe(true)

    retryLoad()
    current_error = { code: 4, message: 'No compatible source was found for this media.' }
    vi.advanceTimersByTime(900)
    expect(showErrorOverlay.value).toBe(true)
    expect(retryCount.value).toBe(3)
    expect(canRetry.value).toBe(false)
  })
})