feat: 优化播放器;添加历史视频
This commit is contained in:
parent
8e6d7fe702
commit
a2770765d8
@ -160,8 +160,10 @@
|
||||
}
|
||||
.video-player{
|
||||
.video-js{
|
||||
@apply w-full h-full;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
background:#fff; // hsl(210, 100%, 48%)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ type StateUpdate = Partial<State> | ((prev: State) => Partial<State>)
|
||||
type Props = {
|
||||
url?: string; cover?: string; showControls?: boolean; className?: string;
|
||||
onChange?: (state: State) => void;
|
||||
muted?: boolean;
|
||||
}
|
||||
export type PlayerInstance = {
|
||||
play: (url: string, currentTime: number) => void;
|
||||
@ -47,18 +48,20 @@ export const Player = React.forwardRef<PlayerInstance, Props>((props, ref) => {
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (tcPlayer) tcPlayer.unload()
|
||||
}
|
||||
}, [])
|
||||
React.useImperativeHandle(ref, () => {
|
||||
return {
|
||||
play: (url, currentTime = 0) => {
|
||||
const player = tcPlayer || TCPlayer(
|
||||
'player-container-id',
|
||||
{
|
||||
sources: [{src: url}],
|
||||
controls: false,
|
||||
const playerVideo = document.createElement('video');
|
||||
const playerId = `player-container-${Date.now().toString(16)}`;
|
||||
playerVideo.setAttribute('id', playerId)
|
||||
playerVideo.setAttribute('preload', 'auto')
|
||||
playerVideo.setAttribute('playsInline', 'true')
|
||||
playerVideo.setAttribute('webkit-playsinline', 'true')
|
||||
if(props.className) playerVideo.setAttribute('className', props.className)
|
||||
document.querySelector('.video-player-container-inner').appendChild(playerVideo)
|
||||
|
||||
const player = TCPlayer(playerId, {
|
||||
//sources: [{src: props.url}],
|
||||
controls: props.showControls,
|
||||
// muted:props.muted,
|
||||
autoplay: true,
|
||||
licenseUrl: 'https://license.vod2.myqcloud.com/license/v2/1328581896_1/v_cube.license'
|
||||
}
|
||||
)
|
||||
@ -74,12 +77,23 @@ export const Player = React.forwardRef<PlayerInstance, Props>((props, ref) => {
|
||||
player.on('error', () => {
|
||||
setState({end: false, playing: false, error: true})
|
||||
})
|
||||
if (!tcPlayer) {
|
||||
setTcPlayer(() => player)
|
||||
return () => {
|
||||
if (tcPlayer) {
|
||||
tcPlayer.pause()
|
||||
tcPlayer.unload()
|
||||
}
|
||||
if(prevUrl == url){
|
||||
}
|
||||
}, [])
|
||||
React.useImperativeHandle(ref, () => {
|
||||
return {
|
||||
play: (url, currentTime = 0) => {
|
||||
console.log('play', url, currentTime)
|
||||
if (!tcPlayer) return;
|
||||
const player = tcPlayer
|
||||
if (prevUrl == url) {
|
||||
player.currentTime(0)
|
||||
}else{
|
||||
} else {
|
||||
player.src(url)
|
||||
}
|
||||
player.play()
|
||||
@ -92,9 +106,6 @@ export const Player = React.forwardRef<PlayerInstance, Props>((props, ref) => {
|
||||
}
|
||||
})
|
||||
|
||||
return <div className={`video-player relative ${props.className}`}>
|
||||
<video id="player-container-id" className="" preload="auto"
|
||||
playsInline webkit-playsinline="true">
|
||||
</video>
|
||||
return <div className={`video-player relative ${props.className} video-player-container-inner`}>
|
||||
</div>
|
||||
})
|
@ -3,44 +3,33 @@ import {useSetState} from "ahooks";
|
||||
import {PlayCircleOutlined} from "@ant-design/icons";
|
||||
import {SearchListTimes} from "@/pages/news/components/news-source.ts";
|
||||
|
||||
type SearchParams = {
|
||||
keywords?: string;
|
||||
date?: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
onSearch?: (params: SearchParams) => Promise<void>;
|
||||
onSearch?: (params: VideoSearchParams) => void;
|
||||
onBtnStartClick?: () => Promise<void>;
|
||||
loading?:boolean;
|
||||
}
|
||||
export default function SearchForm({onSearch, onBtnStartClick}: Props) {
|
||||
export default function SearchForm({onSearch, onBtnStartClick,loading}: Props) {
|
||||
const [state, setState] = useSetState<{
|
||||
timeRange: string;
|
||||
keywords: string;
|
||||
searching: boolean;
|
||||
time: number;
|
||||
}>({
|
||||
keywords: "", searching: false, timeRange: "", time: 0
|
||||
})
|
||||
const onFinish = (values: any) => {
|
||||
setState({searching: true})
|
||||
pushing?: boolean;
|
||||
}>({})
|
||||
const onFinish = (values) => {
|
||||
onSearch?.({
|
||||
keywords: values.keywords,
|
||||
date: values.timeRange.join('-'),
|
||||
}).finally(() => {
|
||||
setState({searching: false})
|
||||
...values,
|
||||
pagination: {page: 1, limit: 10}
|
||||
})
|
||||
//console.log(values)
|
||||
}
|
||||
|
||||
return (<div className={'search-panel'}>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="search-form">
|
||||
<Form className={""} layout="inline" onFinish={onFinish}>
|
||||
<Form.Item name="keywords">
|
||||
<Input className="w-[200px]" placeholder={'请输入搜索信息'}/>
|
||||
<Form<VideoSearchParams> className={""} layout="inline" onFinish={onFinish} initialValues={{title:'',time_flag:0}}>
|
||||
<Form.Item name="title">
|
||||
<Input className="w-[200px]" allowClear placeholder={'请输入搜索信息'}/>
|
||||
</Form.Item>
|
||||
<Form.Item label={'更新时间'} name="date" className="w-[250px]">
|
||||
<Form.Item label={'更新时间'} name="time_flag" className="w-[250px]">
|
||||
<Select
|
||||
defaultValue={state.time} options={SearchListTimes}
|
||||
options={SearchListTimes}
|
||||
optionRender={(option) => (
|
||||
<div className="flex items-center">
|
||||
<span role="icon" className={`radio-icon`}></span>
|
||||
@ -49,19 +38,16 @@ export default function SearchForm({onSearch, onBtnStartClick}: Props) {
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
{/*<Form.Item label={'更新时间'} name="timeRange">*/}
|
||||
{/* <DatePicker.RangePicker />*/}
|
||||
{/*</Form.Item>*/}
|
||||
<Form.Item>
|
||||
<Space size={10}>
|
||||
<Button type={'primary'} htmlType={'submit'}>搜索</Button>
|
||||
<Button loading={loading} type={'primary'} htmlType={'submit'}>搜索</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
<Space size={10}>
|
||||
<Button
|
||||
loading={state.searching} type={'primary'}
|
||||
loading={state.pushing} type={'primary'}
|
||||
onClick={onBtnStartClick} icon={<PlayCircleOutlined/>}
|
||||
>一键推流</Button>
|
||||
</Space>
|
||||
|
@ -4,6 +4,8 @@ import {useState} from "react";
|
||||
|
||||
|
||||
import ImageCover from './cover.png'
|
||||
import {formatDuration, timeFromNow} from "@/util/strings.ts";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
type VideoItemProps = {
|
||||
videoInfo: VideoInfo;
|
||||
@ -32,13 +34,13 @@ export default function VideoItem(props: VideoItemProps) {
|
||||
<Image className={'w-full cursor-pointer'} preview={false} src={ImageCover}/>
|
||||
</div>
|
||||
<div className="text-sm py-2 px-3">
|
||||
<div className="title my-1 cursor-pointer" onClick={props.onClick}>把丰碑立在人民心中</div>
|
||||
<div className="title my-1 cursor-pointer" onClick={props.onClick}>{props.videoInfo.title}</div>
|
||||
<div className="info flex justify-between gap-2 text-sm">
|
||||
<div className="video-time-info text-gray-500">
|
||||
<span>时长: 2年半</span>
|
||||
<span className="ml-1">16小时前</span>
|
||||
<span>时长: {formatDuration(Math.ceil(props.videoInfo.duration / 1000))}</span>
|
||||
<span className="ml-1">{timeFromNow(props.videoInfo.publish_time)}</span>
|
||||
</div>
|
||||
{props.onLive && <div className="live-info">
|
||||
{props.videoInfo.status == 3 && <div className="live-info">
|
||||
<Tag color="processing" className="mr-0">已在直播间</Tag>
|
||||
</div>}
|
||||
</div>
|
||||
|
@ -1,17 +1,24 @@
|
||||
import {useState} from "react";
|
||||
import {Modal, Pagination} from "antd";
|
||||
import {Empty, Modal, Pagination} from "antd";
|
||||
import {useRequest} from "ahooks";
|
||||
|
||||
import VideoItem from "@/pages/library/components/video-item.tsx";
|
||||
import SearchForm from "@/pages/library/components/search-form.tsx";
|
||||
import VideoDetail from "@/pages/library/components/video-detail.tsx";
|
||||
import {getList} from "@/service/api/video.ts";
|
||||
import {search} from "@/service/api/video.ts";
|
||||
|
||||
export default function LibraryIndex() {
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
||||
const {data} = useRequest(()=>getList(),{
|
||||
|
||||
const [params, setParams] = useState<VideoSearchParams>({
|
||||
time_flag: 0,
|
||||
pagination: {
|
||||
page: 1,
|
||||
limit: 10
|
||||
}
|
||||
})
|
||||
const {data,loading} = useRequest(() => search(params), {
|
||||
refreshDeps: [params]
|
||||
})
|
||||
const handleRemove = (video: VideoInfo) => {
|
||||
modal.confirm({
|
||||
@ -39,9 +46,9 @@ export default function LibraryIndex() {
|
||||
{contextHolder}
|
||||
<div className="search-form-container mb-5">
|
||||
<SearchForm
|
||||
onSearch={async () => {
|
||||
}}
|
||||
onSearch={setParams}
|
||||
onBtnStartClick={handleLive}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-white rounded p-5">
|
||||
@ -61,7 +68,21 @@ export default function LibraryIndex() {
|
||||
))}
|
||||
</div>
|
||||
<div className="video-page-container flex justify-center mt-5">
|
||||
<Pagination defaultCurrent={1} total={50}/>
|
||||
{data?.pagination && data?.pagination.total > 0 ? <div className="flex justify-center mt-10">
|
||||
<Pagination
|
||||
current={params.pagination.page}
|
||||
total={data?.pagination.total}
|
||||
pageSize={data?.pagination.limit}
|
||||
showSizeChanger={false}
|
||||
simple={true}
|
||||
rootClassName={'simple-pagination'}
|
||||
onChange={(page) => setParams(prev=>({...prev,pagination: {page, limit: 10}}))}
|
||||
/>
|
||||
</div> : <div className="py-10">
|
||||
<Empty />
|
||||
</div>
|
||||
}
|
||||
{/*<Pagination defaultCurrent={1} total={50}/>*/}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -11,10 +11,13 @@ import ButtonBatch from "@/components/button-batch.tsx";
|
||||
import FlvJs from "flv.js";
|
||||
import {formatDuration} from "@/util/strings.ts";
|
||||
import {useSetState} from "ahooks";
|
||||
import {Player, PlayerInstance} from "@/components/video/player.tsx";
|
||||
|
||||
const cache: { flvPlayer?: FlvJs.Player,timerPlayNext?:any,timerLoadState?:any,prevUrl?:string } = {}
|
||||
const cache: { flvPlayer?: FlvJs.Player, timerPlayNext?: any, timerLoadState?: any, prevUrl?: string } = {}
|
||||
export default function LiveIndex() {
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null)
|
||||
|
||||
const player = useRef<PlayerInstance | null>(null)
|
||||
const [videoData, setVideoData] = useState<LiveVideoInfo[]>([])
|
||||
const [modal, contextHolder] = Modal.useModal()
|
||||
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
||||
@ -25,9 +28,9 @@ export default function LiveIndex() {
|
||||
muted: true,
|
||||
})
|
||||
const activeIndex = useRef(state.activeIndex)
|
||||
useEffect(()=>{
|
||||
useEffect(() => {
|
||||
activeIndex.current = state.activeIndex
|
||||
},[state.activeIndex])
|
||||
}, [state.activeIndex])
|
||||
|
||||
const showVideoItem = (index: number) => {
|
||||
// 找到对应video item 并显示在视图可见区域
|
||||
@ -51,7 +54,7 @@ export default function LiveIndex() {
|
||||
const activeToNext = (index?: number) => {
|
||||
const endToFirst = index != undefined && index > -1 ? false : activeIndex.current >= videoData.length - 1
|
||||
const _activeIndex = index != undefined && index > -1 ? index : (endToFirst ? 0 : activeIndex.current + 1)
|
||||
setState({activeIndex:_activeIndex})
|
||||
setState({activeIndex: _activeIndex})
|
||||
if (endToFirst) {
|
||||
showToast('即将播放第一条视频');
|
||||
}
|
||||
@ -60,51 +63,25 @@ export default function LiveIndex() {
|
||||
return _activeIndex;
|
||||
}
|
||||
const playVideo = (video: LiveVideoInfo, liveState: LiveState) => {
|
||||
if (videoRef.current && video.video_oss_url) {
|
||||
if(cache.timerPlayNext) clearTimeout(cache.timerPlayNext)
|
||||
if (player.current && video.video_oss_url) {
|
||||
if (cache.timerPlayNext) clearTimeout(cache.timerPlayNext)
|
||||
const duration = Math.ceil(video.video_duration / 1000)
|
||||
const playedTime =( Date.now() / 1000 >> 0) - liveState.live_start_time
|
||||
const playedTime = (Date.now() / 1000 >> 0) - liveState.live_start_time
|
||||
if (playedTime < 0 || playedTime > duration) { // 已播放时间大于总时长了
|
||||
//initPlayingState() // 重新获取播放状态
|
||||
return;
|
||||
}
|
||||
if (/mp4$/i.test(video.video_oss_url)) {
|
||||
videoRef.current!.src = video.video_oss_url
|
||||
if(liveState.live_start_time > 0 && playedTime > 0) videoRef.current!.currentTime = playedTime
|
||||
videoRef.current!.play()
|
||||
return;
|
||||
}
|
||||
if (FlvJs.isSupported()) {
|
||||
if(cache.prevUrl !== video.video_oss_url) {
|
||||
// 已经有播放实例 则销毁
|
||||
if (cache.flvPlayer) {
|
||||
cache.flvPlayer.pause()
|
||||
cache.flvPlayer.unload()
|
||||
}
|
||||
cache.prevUrl = video.video_oss_url
|
||||
cache.flvPlayer = FlvJs.createPlayer({
|
||||
type: 'flv',
|
||||
url: video.video_oss_url
|
||||
})
|
||||
player.current?.play(video.video_oss_url, playedTime)
|
||||
cache.timerPlayNext = setTimeout(() => {
|
||||
const index = activeToNext(), nextVideo = videoData[index]
|
||||
playVideo(nextVideo, {live_start_time: (Date.now() / 1000 >> 0), id: nextVideo.id})
|
||||
}, (duration - playedTime) * 1000)
|
||||
|
||||
cache.flvPlayer.attachMediaElement(videoRef.current!)
|
||||
cache.flvPlayer.load()
|
||||
}
|
||||
|
||||
if(liveState.live_start_time > 0 && playedTime > 0) videoRef.current!.currentTime = playedTime
|
||||
|
||||
cache.flvPlayer!.play()
|
||||
|
||||
cache.timerPlayNext = setTimeout(()=>{
|
||||
const index = activeToNext(),nextVideo = videoData[index]
|
||||
playVideo(nextVideo,{live_start_time:(Date.now() / 1000 >> 0),id:nextVideo.id})
|
||||
},(duration - playedTime) * 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
const initPlayingState = () => {
|
||||
if(cache.timerLoadState) clearTimeout(cache.timerLoadState)
|
||||
if(videoData.length == 0) {
|
||||
if (cache.timerLoadState) clearTimeout(cache.timerLoadState)
|
||||
if (videoData.length == 0) {
|
||||
cache.timerLoadState = setTimeout(initPlayingState, 1000)
|
||||
return;
|
||||
}
|
||||
@ -119,22 +96,22 @@ export default function LiveIndex() {
|
||||
}
|
||||
});
|
||||
}
|
||||
const clearAllTimer = ()=>{
|
||||
if(cache.timerPlayNext) clearTimeout(cache.timerPlayNext)
|
||||
if(cache.timerLoadState) clearTimeout(cache.timerLoadState)
|
||||
const clearAllTimer = () => {
|
||||
if (cache.timerPlayNext) clearTimeout(cache.timerPlayNext)
|
||||
if (cache.timerLoadState) clearTimeout(cache.timerLoadState)
|
||||
}
|
||||
|
||||
const loadList = () => {
|
||||
clearAllTimer();
|
||||
getList().then(res => {
|
||||
// console.log('origin list', res.list.map(s => s.id))
|
||||
setVideoData(()=>(res.list || []))
|
||||
setVideoData(() => (res.list || []))
|
||||
setCheckedIdArray([])
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(initPlayingState,[videoData])
|
||||
useEffect(()=>{
|
||||
useEffect(initPlayingState, [videoData])
|
||||
useEffect(() => {
|
||||
loadList()
|
||||
return clearAllTimer;
|
||||
}, [])
|
||||
@ -186,15 +163,9 @@ export default function LiveIndex() {
|
||||
<div className="video-player-container mr-8 flex flex-col">
|
||||
<div className="text-center text-base">数字人直播间</div>
|
||||
<div className="video-player flex justify-center flex-1 mt-5">
|
||||
<div className="live-player relative rounded overflow-hidden w-[360px] h-[636px]" style={{backgroundColor:'hsl(210, 100%, 48%)'}}>
|
||||
<video ref={videoRef} autoPlay muted={state.muted}
|
||||
className="w-[360px] rounded overflow-hidden h-full object-contain"></video>
|
||||
{state.muted && state.activeIndex != -1 && <div className="absolute inset-0 flex items-center justify-center">
|
||||
<Button onClick={()=>{
|
||||
setState({muted: false})
|
||||
videoRef.current!.muted= false;
|
||||
}}>开启声音</Button>
|
||||
</div>}
|
||||
<div className="live-player relative rounded overflow-hidden w-[360px] h-[636px]"
|
||||
style={{backgroundColor: 'hsl(210, 100%, 48%)'}}>
|
||||
<Player ref={player} className="w-[360px] h-[636px] bg-white" muted={true}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
|
@ -19,7 +19,7 @@ export default function VideoIndex() {
|
||||
const [editId, setEditId] = useState(-1)
|
||||
const [videoData, setVideoData] = useState<VideoInfo[]>([])
|
||||
const [modal, contextHolder] = Modal.useModal()
|
||||
const player = useRef<PlayerInstance>(null)
|
||||
const player = useRef<PlayerInstance|null>(null)
|
||||
const [state, setState] = useSetState({
|
||||
checkedAll: false,
|
||||
playingIndex: -1,
|
||||
@ -45,9 +45,9 @@ export default function VideoIndex() {
|
||||
|
||||
// 播放视频
|
||||
const playVideo = (video: VideoInfo, playingIndex: number) => {
|
||||
if (video.oss_video_url && video.status == 2) {
|
||||
if (video.status !== 1) { // video.oss_video_url &&
|
||||
setState({playingIndex})
|
||||
player.current?.play(video.oss_video_url, 0)
|
||||
player.current?.play(video.oss_video_url||'https://staticplus.gachafun.com/ai-collect/composite_video/2024-12-14/1185251497659736064.flv', 0)
|
||||
}
|
||||
}
|
||||
// 处理全选
|
||||
|
@ -3,7 +3,9 @@ import {post} from "@/service/request.ts";
|
||||
export function getList() {
|
||||
return post<DataList<VideoInfo>>('/video/list')
|
||||
}
|
||||
|
||||
export function search(params:VideoSearchParams) {
|
||||
return post<DataList<VideoInfo>>('/video/search',params)
|
||||
}
|
||||
/**
|
||||
* 视频列表的文章编辑(需要重新生成视频)
|
||||
* @param title
|
||||
|
6
src/types/api.d.ts
vendored
6
src/types/api.d.ts
vendored
@ -80,6 +80,11 @@ declare interface ListCrawlerNewsItem extends BasicArticleInfo {
|
||||
// 内部文章关联id
|
||||
internal_article_id: number;
|
||||
}
|
||||
declare interface VideoSearchParams extends ApiRequestPageParams{
|
||||
// 标题
|
||||
title?: string;
|
||||
time_flag?: number;
|
||||
}
|
||||
declare interface VideoInfo {
|
||||
id: number;
|
||||
video_title: string;
|
||||
@ -89,6 +94,7 @@ declare interface VideoInfo {
|
||||
duration: number;
|
||||
article_id: number;
|
||||
status: number;
|
||||
publish_time?: number|string;
|
||||
}
|
||||
// room live
|
||||
declare interface LiveVideoInfo {
|
||||
|
@ -61,6 +61,7 @@ export function formatTime(time:any,template = 'YYYY-MM-DD HH:mm:ss') {
|
||||
}
|
||||
|
||||
export function timeFromNow(time: any) {
|
||||
if(!time) return '';
|
||||
return getDayjs(time).fromNow();
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user