From c75985188ef703d2a102b924a3573c70663f06d6 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 11 Jul 2025 18:03:11 +0800 Subject: [PATCH] fix: video player --- demo.md | 205 +++++++++++++++++++++++++++++++++ scripts/test_video_fix.md | 187 ++++++++++++++++++++++++++++++ src/components/VideoPlayer.tsx | 52 +++++++-- 3 files changed, 433 insertions(+), 11 deletions(-) create mode 100644 demo.md create mode 100644 scripts/test_video_fix.md diff --git a/demo.md b/demo.md new file mode 100644 index 0000000..9fff764 --- /dev/null +++ b/demo.md @@ -0,0 +1,205 @@ +import React, { useEffect, useImperativeHandle, useRef, useState } from 'react'; +import Hls from 'hls.js'; +import axios from 'axios'; +import { container, CoreEnv } from '@roasmax/core'; +import { snowflake } from '@roasmax/utils'; +import { useQuery } from '@tanstack/react-query'; +import { LocAttachmentController } from '@roasmax/sdk'; + +export interface OnSourceLoaded { + (player: HTMLVideoElement | null): void; +} +export interface OnPlayerError { + (err: Error, player: HTMLVideoElement | null): void; +} +export interface OnPlayerTimeChange { + (player: HTMLVideoElement | null): void; +} +export interface LiveStreamPlayerRef { + seek(time: number): void; + pause(): void; + play(): void; + getCurrentTime(): number; + isPaused(): boolean; + getDuration(): number; + captureFrame(): string | undefined; + fullScreen(): void; +} + +export async function uploadCaptureFrame(content: string, projectId: string): Promise<{ urn: string }> { + const coreEnv = container.resolve(CoreEnv); + return axios + .create({ + baseURL: coreEnv.MODAL_BASE_URL, + headers: { + Authorization: `Bearer ${coreEnv.MODAL_SERVER_KEY || 'bowong7777'}`, + }, + }) + .request({ + url: `/cache/upload-s3-b64`, + method: `post`, + data: { + file: { + raw_content: content, + filename: `images/${projectId}/${snowflake.nextId()}.png`, + content_type: `application/json`, + }, + }, + }) + .then((res) => res.data); +} + +export const LiveStreamPlayer: React.FC<{ + id?: string; + ref?: React.RefObject; + onSourceLoaded: OnSourceLoaded; + onError: OnPlayerError; + onTimeChange: OnPlayerTimeChange; +}> = ({ id, onSourceLoaded, onError, onTimeChange, ref }) => { + const videoRef = useRef(null); + const hlsRef = useRef(null); + const [isLoading, setIsLoading] = useState(true); + + const { data: attachment } = useQuery({ + queryKey: [`generateUrlById`, id], + queryFn: async () => { + if (!id) return ``; + const c = container.resolve(LocAttachmentController); + const attachment = await c.get(id); + return attachment; + }, + }); + + useImperativeHandle(ref, () => ({ + seek: (time: number) => { + videoRef.current && (videoRef.current.currentTime = time); + }, + pause: () => { + videoRef.current?.pause(); + }, + play: () => { + videoRef.current?.play(); + }, + isPaused: () => { + return !!videoRef.current?.paused; + }, + getCurrentTime: () => { + return videoRef.current?.currentTime || 0; + }, + getDuration: () => { + return videoRef.current?.duration || 0; + }, + captureFrame: () => { + const video = videoRef.current; + if (!video) return; + // 创建canvas元素 + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) return; + // 设置canvas尺寸 + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + + // 上传到modal + // 绘制视频帧到canvas + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + const dataURL = canvas.toDataURL('image/jpeg', 0.05); + return dataURL; + }, + fullScreen: () => { + const video = videoRef.current; + if (video) { + video.requestFullscreen(); + } + }, + })); + + useEffect(() => { + const video = videoRef.current; + if (!video) return; + + const showLoading = () => setIsLoading(true); + const hideLoading = () => setIsLoading(false); + + video.addEventListener('waiting', showLoading); + video.addEventListener('seeking', showLoading); + + video.addEventListener('playing', hideLoading); + video.addEventListener('seeked', hideLoading); + video.addEventListener('canplay', hideLoading); + + return () => { + if (!video) return; + video.removeEventListener('waiting', showLoading); + video.removeEventListener('seeking', showLoading); + video.removeEventListener('playing', hideLoading); + video.removeEventListener('seeked', hideLoading); + video.removeEventListener('canplay', hideLoading); + }; + }, []); + + useEffect(() => { + const video = videoRef.current; + if (!video || !attachment) return; + + if (hlsRef.current) { + hlsRef.current.destroy(); + hlsRef.current = null; + } + + video.addEventListener('loadedmetadata', (a) => { + return onSourceLoaded(video); + }); + video.addEventListener('timeupdate', () => onTimeChange(video)); + video.addEventListener('error', () => onError(new Error('Video error'), video)); + if (attachment.type === 'live-stream') { + if (video.canPlayType('application/vnd.apple.mpegurl')) { + // Safari等原生支持HLS + video.src = attachment.url!; + video.poster = attachment.coverUrl!; + } else if (Hls.isSupported()) { + // 其他浏览器用hls.js + video.poster = attachment.coverUrl!; + const hls = new Hls(); + hlsRef.current = hls; + // attachment.url! + console.log(attachment.url); + hls.loadSource(attachment.url!); + hls.attachMedia(video); + hls.on(Hls.Events.ERROR, (event, data) => { + if (data.details === Hls.ErrorDetails.MANIFEST_LOAD_ERROR) { + const corsError = new Error('CORS error: Check server Access-Control-Allow-Origin headers'); + onError(corsError, video); + } else { + onError(new Error(data?.details || 'HLS.js error'), video); + } + }); + } else { + onError(new Error('HLS is not supported in this browser'), video); + } + } else { + video.src = attachment.url!; + video.poster = attachment.coverUrl!; + } + return () => { + if (hlsRef.current) { + hlsRef.current.destroy(); + hlsRef.current = null; + } + if (video) { + video.src = ''; + } + }; + }, [attachment]); + + return ( +
+