206 lines
6.2 KiB
Markdown
206 lines
6.2 KiB
Markdown
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<HTMLVideoElement>(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 (
|
|
<div className="relative h-full w-full">
|
|
<video ref={videoRef} crossOrigin="anonymous" className="h-full w-full bg-[#18181A]" autoPlay preload="auto" />
|
|
{isLoading && (
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
<div className="h-12 w-12 animate-spin rounded-full border-4 border-solid border-white border-t-transparent" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|