mxivideo/demo.md

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>
);
};