视频播放页面

This commit is contained in:
禺狨 2023-06-30 09:49:06 +08:00
parent 58ba3577a8
commit 773ddb8b12
8 changed files with 556 additions and 3 deletions

View File

@ -5,9 +5,15 @@
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<title>PlayEdu</title>
<script src="/js/DPlayer.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script
crossorigin="anonymous"
integrity="sha512-oHrfR/z2wkuRuaHrdZ9NhoT/o/1kteub+QvmQgVzOKK7NTvIKQMvnY9+/RR0+eW311o4lAE/YzzLXXmP2XUvig=="
src="https://lib.baomitu.com/hls.js/1.1.4/hls.min.js"
></script>
</body>
</html>

View File

@ -135,3 +135,8 @@ code {
overflow: hidden;
text-overflow: ellipsis;
}
#meedu-player-container {
width: 100%;
height: 100%;
}

View File

@ -1,5 +1,4 @@
import React from "react";
import { useNavigate } from "react-router-dom";
import styles from "./hour.module.scss";
import { durationFormat } from "../../../utils/index";
@ -10,6 +9,7 @@ interface PropInterface {
duration: number;
record: any;
progress: number;
onSuccess: (cid: number, id: number) => void;
}
export const HourCompenent: React.FC<PropInterface> = ({
@ -19,14 +19,14 @@ export const HourCompenent: React.FC<PropInterface> = ({
duration,
record,
progress,
onSuccess,
}) => {
const navigate = useNavigate();
return (
<>
<div
className={styles["item"]}
onClick={() => {
navigate(`/course/${cid}/hour/${id}`);
onSuccess(cid, id);
}}
>
<div className={styles["top-item"]}>

View File

@ -0,0 +1,64 @@
import React from "react";
import styles from "./hour.module.scss";
import { durationFormat } from "../../../utils/index";
interface PropInterface {
id: number;
cid: number;
vid: number;
title: string;
duration: number;
record: any;
progress: number;
onSuccess: (cid: number, id: number) => void;
}
export const HourCompenent: React.FC<PropInterface> = ({
id,
cid,
vid,
title,
duration,
record,
progress,
onSuccess,
}) => {
return (
<>
<div
className={styles["item"]}
onClick={() => {
onSuccess(cid, id);
}}
>
<div className={styles["top-item"]}>
<div className="d-flex">
<i className="iconfont icon-icon-video"></i>
<span className={styles["label"]}></span>
</div>
{vid === id && (
<div className={styles["studying"]}>
<span></span>
</div>
)}
{vid !== id && progress > 0 && progress < 100 && (
<div className={styles["studying"]}>
<span>
{durationFormat(Number(record.finished_duration || 0))}
</span>
</div>
)}
{vid !== id && progress >= 100 && (
<div className={styles["complete"]}>
<span></span>{" "}
</div>
)}
</div>
<div className={styles["title"]}>
{title}({durationFormat(Number(duration))})
</div>
</div>
</>
);
};

View File

@ -42,6 +42,10 @@ const CoursePage = () => {
});
};
const playVideo = (cid: number, id: number) => {
navigate(`/course/${cid}/hour/${id}`);
};
return (
<div className="main-body">
<div className="main-header" style={{ backgroundColor: "#FF4D4F" }}>
@ -140,6 +144,9 @@ const CoursePage = () => {
(learnHourRecord[item.id].finished_duration * 100) /
learnHourRecord[item.id].total_duration
}
onSuccess={(cid: number, id: number) => {
playVideo(cid, id);
}}
></HourCompenent>
)}
{!learnHourRecord[item.id] && (
@ -150,6 +157,9 @@ const CoursePage = () => {
record={null}
duration={item.duration}
progress={0}
onSuccess={(cid: number, id: number) => {
playVideo(cid, id);
}}
></HourCompenent>
)}
</div>
@ -175,6 +185,9 @@ const CoursePage = () => {
(learnHourRecord[it.id].finished_duration * 100) /
learnHourRecord[it.id].total_duration
}
onSuccess={(cid: number, id: number) => {
playVideo(cid, id);
}}
></HourCompenent>
)}
{!learnHourRecord[it.id] && (
@ -185,6 +198,9 @@ const CoursePage = () => {
record={null}
duration={it.duration}
progress={0}
onSuccess={(cid: number, id: number) => {
playVideo(cid, id);
}}
></HourCompenent>
)}
</div>

View File

@ -0,0 +1,93 @@
.video-body {
width: 100%;
float: left;
height: auto;
position: relative;
display: flex;
flex-direction: column;
box-sizing: border-box;
overflow-x: hidden;
overflow-y: auto;
.back-icon {
width: 30px;
height: 30px;
position: absolute;
top: 12px;
left: 20px;
z-index: 999;
}
.video-box {
width: 100%;
float: left;
height: 211px;
position: relative;
.alert-message {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 18px;
color: white;
z-index: 100;
.alert-button {
width: 100px;
height: 36px;
background: #ff4d4f;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin-bottom: 0px;
font-size: 14px;
font-weight: 500;
color: #ffffff;
}
}
}
}
.chapters-hours-cont {
width: 100%;
height: auto;
box-sizing: border-box;
padding: 10px 20px 20px 20px;
.hours-list-box {
width: 100%;
height: auto;
display: flex;
flex-direction: column;
.chapter-it {
width: 100%;
height: auto;
margin-top: 20px;
.chapter-name {
width: 100%;
height: 15px;
font-size: 15px;
font-weight: 500;
color: rgba(0, 0, 0, 0.88);
line-height: 15px;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.hours-it {
width: 100%;
height: auto;
margin-top: 20px;
}
}
}

364
src/pages/course/video.tsx Normal file
View File

@ -0,0 +1,364 @@
import { useEffect, useRef, useState } from "react";
import styles from "./video.module.scss";
import { useParams, useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { course as Course } from "../../api/index";
import { Toast, Image } from "antd-mobile";
import backIcon from "../../assets/images/commen/icon-back-n.png";
import { Empty } from "../../components";
import { HourCompenent } from "./compenents/videoHour";
declare const window: any;
const CoursePlayPage = () => {
const navigate = useNavigate();
const params = useParams();
const systemConfig = useSelector((state: any) => state.systemConfig.value);
const user = useSelector((state: any) => state.loginUser.value.user);
const [playUrl, setPlayUrl] = useState<string>("");
const [playDuration, setPlayDuration] = useState(0);
const [playendedStatus, setPlayendedStatus] = useState<Boolean>(false);
const [lastSeeValue, setLastSeeValue] = useState({});
const [course, setCourse] = useState<any>({});
const [hour, setHour] = useState<any>({});
const [loading, setLoading] = useState<Boolean>(false);
const [isLastpage, setIsLastpage] = useState<Boolean>(false);
const [totalHours, setTotalHours] = useState<any>([]);
const [playingTime, setPlayingTime] = useState(0);
const [watchedSeconds, setWatchedSeconds] = useState(0);
const [chapters, setChapters] = useState<any>([]);
const [hours, setHours] = useState<any>({});
const [learnRecord, setLearnRecord] = useState<any>({});
const [learnHourRecord, setLearnHourRecord] = useState<any>({});
const myRef = useRef(0);
const playRef = useRef(0);
const watchRef = useRef(0);
const totalRef = useRef(0);
useEffect(() => {
getCourse();
getDetail();
}, [params.courseId, params.hourId]);
useEffect(() => {
myRef.current = playDuration;
}, [playDuration]);
useEffect(() => {
playRef.current = playingTime;
}, [playingTime]);
useEffect(() => {
watchRef.current = watchedSeconds;
}, [watchedSeconds]);
useEffect(() => {
totalRef.current = hour.duration;
}, [hour]);
const getCourse = () => {
Course.detail(Number(params.courseId)).then((res: any) => {
setChapters(res.data.chapters);
setHours(res.data.hours);
if (res.data.learn_record) {
setLearnRecord(res.data.learn_record);
}
if (res.data.learn_hour_records) {
setLearnHourRecord(res.data.learn_hour_records);
}
let totalHours: any = [];
if (res.data.chapters.length === 0) {
setTotalHours(res.data.hours[0]);
totalHours = res.data.hours[0];
} else if (res.data.chapters.length > 0) {
const arr: any = [];
for (let key in res.data.hours) {
res.data.hours[key].map((item: any) => {
arr.push(item);
});
}
setTotalHours(arr);
totalHours = arr;
}
const index = totalHours.findIndex(
(i: any) => i.id === Number(params.hourId)
);
if (index === totalHours.length - 1) {
setIsLastpage(true);
}
});
};
const getDetail = () => {
if (loading) {
return true;
}
setLoading(true);
Course.play(Number(params.courseId), Number(params.hourId))
.then((res: any) => {
setCourse(res.data.course);
setHour(res.data.hour);
document.title = res.data.hour.title;
let record = res.data.user_hour_record;
let params = null;
if (record && record.finished_duration && record.is_finished === 0) {
params = {
time: 5,
pos: record.finished_duration,
};
setLastSeeValue(params);
setWatchedSeconds(record.finished_duration);
} else if (record && record.is_finished === 1) {
setWatchedSeconds(res.data.hour.duration);
}
getVideoUrl(params);
setLoading(false);
})
.catch((e) => {
setLoading(false);
});
};
const getVideoUrl = (data: any) => {
Course.playUrl(Number(params.courseId), Number(params.hourId)).then(
(res: any) => {
setPlayUrl(res.data.url);
initDPlayer(res.data.url, 0, data);
}
);
};
const initDPlayer = (playUrl: string, isTrySee: number, params: any) => {
let banDrag =
systemConfig.playerIsDisabledDrag &&
watchRef.current < totalRef.current &&
watchRef.current === 0;
window.player = new window.DPlayer({
container: document.getElementById("meedu-player-container"),
autoplay: false,
video: {
url: playUrl,
pic: systemConfig.playerPoster,
},
try: isTrySee === 1,
bulletSecret: {
enabled: systemConfig.playerIsEnabledBulletSecret,
text: systemConfig.playerBulletSecretText
.replace("{name}", user.name)
.replace("{email}", user.email)
.replace("{idCard}", user.id_card),
size: "14px",
color: systemConfig.playerBulletSecretColor || "red",
opacity: Number(systemConfig.playerBulletSecretOpacity),
},
ban_drag: banDrag,
last_see_pos: params,
});
// 监听播放进度更新evt
window.player.on("timeupdate", () => {
let currentTime = parseInt(window.player.video.currentTime);
if (
systemConfig.playerIsDisabledDrag &&
watchRef.current < totalRef.current &&
currentTime - playRef.current >= 2 &&
currentTime > watchRef.current
) {
Toast.show("首次学习禁止快进");
window.player.seek(watchRef.current);
} else {
setPlayingTime(currentTime);
playTimeUpdate(parseInt(window.player.video.currentTime), false);
}
});
window.player.on("ended", () => {
if (
systemConfig.playerIsDisabledDrag &&
watchRef.current < totalRef.current &&
window.player.video.duration - playRef.current >= 2
) {
window.player.seek(playRef.current);
return;
}
setPlayingTime(0);
setPlayendedStatus(true);
playTimeUpdate(parseInt(window.player.video.currentTime), true);
exitFullscreen();
window.player && window.player.destroy();
});
setLoading(false);
};
const playTimeUpdate = (duration: number, isEnd: boolean) => {
if (duration - myRef.current >= 10 || isEnd === true) {
setPlayDuration(duration);
Course.record(
Number(params.courseId),
Number(params.hourId),
duration
).then((res: any) => {});
Course.playPing(Number(params.courseId), Number(params.hourId)).then(
(res: any) => {}
);
}
};
const goNextVideo = () => {
const index = totalHours.findIndex(
(i: any) => i.id === Number(params.hourId)
);
if (index === totalHours.length - 1) {
setIsLastpage(true);
Toast.show("已经是最后一节了!");
} else if (index < totalHours.length - 1) {
navigate(`/course/${params.courseId}/hour/${totalHours[index + 1].id}`, {
replace: true,
});
}
};
const exitFullscreen = () => {
let de: any;
de = document;
if (de.fullscreenElement !== null) {
de.exitFullscreen();
} else if (de.mozCancelFullScreen) {
de.mozCancelFullScreen();
} else if (de.webkitCancelFullScreen) {
de.webkitCancelFullScreen();
}
};
const playVideo = (cid: number, id: number) => {
window.player && window.player.destroy();
navigate(`/course/${cid}/hour/${id}`, { replace: true });
};
return (
<div className="main-body">
<div className={styles["video-body"]}>
<Image
className={styles["back-icon"]}
src={backIcon}
onClick={() => navigate(-1)}
/>
<div className={styles["video-box"]}>
<div className="play-box" id="meedu-player-container"></div>
{playendedStatus && (
<div className={styles["alert-message"]}>
{isLastpage && (
<div
className={styles["alert-button"]}
onClick={() => navigate(`/course/${params.courseId}`)}
>
</div>
)}
{!isLastpage && (
<div
className={styles["alert-button"]}
onClick={() => {
window.player && window.player.destroy();
setLastSeeValue({});
setPlayendedStatus(false);
goNextVideo();
}}
>
</div>
)}
</div>
)}
</div>
</div>
<div className={styles["chapters-hours-cont"]}>
{chapters.length === 0 && JSON.stringify(hours) === "{}" && <Empty />}{" "}
{chapters.length === 0 && JSON.stringify(hours) !== "{}" && (
<div className={styles["hours-list-box"]}>
{hours[0].map((item: any, index: number) => (
<div key={item.id} className={styles["hours-it"]}>
{learnHourRecord[item.id] && (
<HourCompenent
id={item.id}
cid={item.course_id}
title={item.title}
record={learnHourRecord[item.id]}
duration={item.duration}
vid={Number(params.hourId)}
progress={
(learnHourRecord[item.id].finished_duration * 100) /
learnHourRecord[item.id].total_duration
}
onSuccess={(cid: number, id: number) => {
playVideo(cid, id);
}}
></HourCompenent>
)}
{!learnHourRecord[item.id] && (
<HourCompenent
id={item.id}
cid={item.course_id}
title={item.title}
record={null}
duration={item.duration}
vid={Number(params.hourId)}
progress={0}
onSuccess={(cid: number, id: number) => {
playVideo(cid, id);
}}
></HourCompenent>
)}
</div>
))}
</div>
)}
{chapters.length > 0 && JSON.stringify(hours) !== "{}" && (
<div className={styles["hours-list-box"]}>
{chapters.map((item: any, index: number) => (
<div key={item.id} className={styles["chapter-it"]}>
<div className={styles["chapter-name"]}>{item.name}</div>
{hours[item.id] &&
hours[item.id].map((it: any, int: number) => (
<div key={it.id} className={styles["hours-it"]}>
{learnHourRecord[it.id] && (
<HourCompenent
id={it.id}
cid={item.course_id}
title={it.title}
record={learnHourRecord[it.id]}
duration={it.duration}
vid={Number(params.hourId)}
progress={
(learnHourRecord[it.id].finished_duration * 100) /
learnHourRecord[it.id].total_duration
}
onSuccess={(cid: number, id: number) => {
playVideo(cid, id);
}}
></HourCompenent>
)}
{!learnHourRecord[it.id] && (
<HourCompenent
id={it.id}
cid={item.course_id}
title={it.title}
record={null}
duration={it.duration}
vid={Number(params.hourId)}
progress={0}
onSuccess={(cid: number, id: number) => {
playVideo(cid, id);
}}
></HourCompenent>
)}
</div>
))}
</div>
))}
</div>
)}
</div>
</div>
);
};
export default CoursePlayPage;

View File

@ -11,6 +11,7 @@ import ChangePasswordPage from "../pages/change-password/index";
import ChangeDepartmentPage from "../pages/change-department/index";
import StudyPage from "../pages/study/index";
import CoursePage from "../pages/course/index";
import CoursePlayPage from "../pages/course/video";
import PrivateRoute from "../components/private-route";
let RootPage: any = null;
@ -67,6 +68,10 @@ const routes: RouteObject[] = [
path: "/course/:courseId",
element: <PrivateRoute Component={<CoursePage />} />,
},
{
path: "/course/:courseId/hour/:hourId",
element: <PrivateRoute Component={<CoursePlayPage />} />,
},
],
},
];