6.2 KiB
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<LiveStreamPlayerRef | null>; onSourceLoaded: OnSourceLoaded; onError: OnPlayerError; onTimeChange: OnPlayerTimeChange; }> = ({ id, onSourceLoaded, onError, onTimeChange, ref }) => { const videoRef = useRef(null); const hlsRef = useRef<Hls | null>(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 (