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 (