index.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. import {
  2. defineComponent,
  3. onMounted,
  4. onUnmounted,
  5. reactive,
  6. toRefs
  7. } from 'vue';
  8. import { ref } from 'vue';
  9. import TCPlayer from 'tcplayer.js';
  10. import 'tcplayer.js/dist/tcplayer.min.css';
  11. import styles from './index.module.less';
  12. import iconplay from '@views/attend-class/image/icon-pause.png';
  13. import iconpause from '@views/attend-class/image/icon-play.png';
  14. import iconReplay from '@views/attend-class/image/icon-replay.png';
  15. import iconPreviewDownload from '@views/attend-class/image/icon-preivew-download2.png';
  16. import iconFullscreen from '@views/attend-class/image/icon-fullscreen.png';
  17. import iconFullscreenExit from '@views/attend-class/image/icon-fullscreen-exit.png';
  18. import iconSpeed from '@views/attend-class/image/icon-speed.png';
  19. import { NSlider, useMessage } from 'naive-ui';
  20. import { saveAs } from 'file-saver';
  21. import { exitFullscreen } from '/src/utils';
  22. export default defineComponent({
  23. name: 'video-play',
  24. props: {
  25. src: {
  26. type: String,
  27. default: ''
  28. },
  29. title: {
  30. type: String,
  31. default: ''
  32. },
  33. poster: {
  34. type: String,
  35. default: ''
  36. },
  37. isEmtry: {
  38. type: Boolean,
  39. default: false
  40. },
  41. isDownload: {
  42. type: Boolean,
  43. default: false
  44. },
  45. fullscreen: {
  46. type: Boolean,
  47. default: false
  48. }
  49. },
  50. emits: ['loadedmetadata', 'togglePlay', 'ended', 'reset'],
  51. setup(props, { emit, expose }) {
  52. const message = useMessage();
  53. const videoId =
  54. 'vFullscreen' + Date.now() + Math.floor(Math.random() * 100);
  55. const { src, poster, isEmtry } = toRefs(props);
  56. const videoFroms = reactive({
  57. isFullScreen: false, // 是否全屏
  58. paused: true,
  59. currentTimeNum: 0,
  60. currentTime: '00:00',
  61. durationNum: 0,
  62. duration: '00:00',
  63. showBar: true,
  64. timer: null as any,
  65. speedControl: false,
  66. speedStyle: {
  67. left: '1px'
  68. },
  69. defaultSpeed: 1 // 默认速度
  70. });
  71. const videoRef = ref();
  72. const videoItem = ref();
  73. const videoID = 'video' + Date.now() + Math.floor(Math.random() * 100);
  74. // 对时间进行格式化
  75. const timeFormat = (num: number) => {
  76. if (num > 0) {
  77. const m = Math.floor(num / 60);
  78. const s = num % 60;
  79. return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
  80. } else {
  81. return '00:00';
  82. }
  83. };
  84. //
  85. const toggleHideControl = (isShow = false) => {
  86. videoFroms.showBar = isShow;
  87. videoFroms.speedControl = false;
  88. };
  89. const onReplay = () => {
  90. videoFroms.speedControl = false;
  91. if (!videoItem.value) return;
  92. videoItem.value.currentTime(0);
  93. };
  94. // 切换音频播放
  95. const onToggleVideo = (e?: MouseEvent) => {
  96. videoFroms.speedControl = false;
  97. e?.stopPropagation();
  98. if (videoFroms.paused) {
  99. videoItem.value.play();
  100. videoFroms.paused = false;
  101. } else {
  102. videoItem.value.pause();
  103. videoFroms.paused = true;
  104. }
  105. emit('togglePlay', videoFroms.paused);
  106. };
  107. // 下载资源
  108. const onDownload = () => {
  109. if (!props.src) {
  110. message.error('下载失败');
  111. return;
  112. }
  113. const fileUrl = props.src;
  114. // 发起Fetch请求
  115. fetch(fileUrl)
  116. .then(response => response.blob())
  117. .then(blob => {
  118. saveAs(blob, props.title || new Date().getTime() + '');
  119. })
  120. .catch(() => {
  121. message.error('下载失败');
  122. });
  123. };
  124. /** 延迟收起模态框 */
  125. const setModelOpen = (status = true) => {
  126. clearTimeout(videoFroms.timer);
  127. toggleHideControl(status);
  128. videoFroms.timer = setTimeout(() => {
  129. toggleHideControl(false);
  130. }, 3000);
  131. };
  132. const __init = () => {
  133. if (videoItem.value) {
  134. videoItem.value.poster(poster.value); // 封面
  135. videoItem.value.src(isEmtry.value ? '' : src.value); // url 播放地址
  136. // 初步加载时
  137. videoItem.value.one('loadedmetadata', () => {
  138. console.log(' Loading metadata');
  139. // 获取时长
  140. videoFroms.duration = timeFormat(
  141. Math.round(videoItem.value.duration())
  142. );
  143. videoFroms.durationNum = videoItem.value.duration();
  144. emit('loadedmetadata', videoItem.value);
  145. onToggleVideo();
  146. });
  147. // 视频播放时加载
  148. videoItem.value.on('timeupdate', () => {
  149. videoFroms.currentTime = timeFormat(
  150. Math.round(videoItem.value?.currentTime() || 0)
  151. );
  152. videoFroms.currentTimeNum = videoItem.value.currentTime();
  153. });
  154. // 视频播放结束
  155. videoItem.value.on('ended', () => {
  156. videoFroms.paused = true;
  157. emit('ended');
  158. });
  159. videoItem.value.on('play', () => {
  160. setModelOpen();
  161. });
  162. videoItem.value.on('pause', () => {
  163. clearTimeout(videoFroms.timer);
  164. videoFroms.showBar = true;
  165. });
  166. }
  167. };
  168. /** 全屏 */
  169. const isElementFullscreen = (element: any) => {
  170. const dom: any = document;
  171. return (
  172. element ===
  173. (dom.fullscreenElement ||
  174. dom.webkitFullscreenElement ||
  175. dom.mozFullScreenElement ||
  176. dom.msFullscreenElement)
  177. );
  178. };
  179. const onFullScreen = () => {
  180. const el: any = document.querySelector('#' + videoId);
  181. // //进入全屏
  182. if (el) {
  183. if (isElementFullscreen(el)) {
  184. exitFullscreen();
  185. } else {
  186. (el.requestFullscreen && el.requestFullscreen()) ||
  187. (el.mozRequestFullScreen && el.mozRequestFullScreen()) ||
  188. (el.webkitRequestFullscreen && el.webkitRequestFullscreen()) ||
  189. (el.msRequestFullscreen && el.msRequestFullscreen());
  190. }
  191. // videoFroms.isFullScreen = isElementFullscreen(el);
  192. }
  193. };
  194. const onFullScreenChange = () => {
  195. if (document.fullscreenElement) {
  196. videoFroms.isFullScreen = true;
  197. } else {
  198. videoFroms.isFullScreen = false;
  199. }
  200. };
  201. onMounted(() => {
  202. videoItem.value = TCPlayer(videoID, {
  203. appID: '',
  204. controls: false,
  205. autoPlay: true
  206. }); // player-container-id 为播放器容器 ID,必须与 html 中一致
  207. __init();
  208. window.addEventListener('fullscreenchange', onFullScreenChange);
  209. });
  210. onUnmounted(() => {
  211. window.removeEventListener('fullscreenchange', onFullScreenChange);
  212. if (videoItem.value) {
  213. videoItem.value.pause();
  214. videoItem.value.dispose();
  215. videoItem.value = null;
  216. }
  217. });
  218. expose({
  219. // changePlayBtn,
  220. toggleHideControl
  221. });
  222. return () => (
  223. <div
  224. class={[styles.videoWrap, 'videoWrapModal']}
  225. id={videoId}
  226. onClick={() => setModelOpen(!videoFroms.showBar)}>
  227. <div class={[styles.videoContent]}>
  228. <video
  229. style={{ width: '100%', height: '100%' }}
  230. src={isEmtry.value ? '' : src.value}
  231. poster={poster.value}
  232. ref={videoRef}
  233. id={videoID}
  234. preload="auto"
  235. playsinline
  236. webkit-playsinline></video>
  237. <div
  238. class={[
  239. styles.controls,
  240. videoFroms.showBar ? '' : styles.sectionAnimate
  241. ]}
  242. onClick={(e: MouseEvent) => {
  243. e.stopPropagation();
  244. emit('reset');
  245. setModelOpen();
  246. }}>
  247. <div class={styles.actions}>
  248. <div class={styles.actionWrap}>
  249. <button class={styles.actionBtn} onClick={onToggleVideo}>
  250. {videoFroms.paused ? (
  251. <img class={styles.playIcon} src={iconplay} />
  252. ) : (
  253. <img class={styles.playIcon} src={iconpause} />
  254. )}
  255. </button>
  256. <button class={styles.iconReplay} onClick={onReplay}>
  257. <img src={iconReplay} />
  258. </button>
  259. <div
  260. class={styles.actionBtnSpeed}
  261. onClick={(e: any) => {
  262. e.stopPropagation();
  263. videoFroms.speedControl = !videoFroms.speedControl;
  264. if (videoFroms.speedControl) {
  265. clearTimeout(videoFroms.timer);
  266. videoFroms.showBar = true;
  267. } else {
  268. setModelOpen(true);
  269. }
  270. }}>
  271. <img src={iconSpeed} />
  272. <div
  273. style={{
  274. display: videoFroms.speedControl ? 'block' : 'none'
  275. }}>
  276. <div
  277. class={styles.sliderPopup}
  278. onClick={(e: Event) => {
  279. e.stopPropagation();
  280. }}>
  281. <i
  282. class={styles.iconAdd}
  283. onClick={() => {
  284. if (videoFroms.defaultSpeed >= 1.5) {
  285. return;
  286. }
  287. if (videoItem.value) {
  288. videoFroms.defaultSpeed =
  289. (videoFroms.defaultSpeed * 10 + 1) / 10;
  290. videoItem.value.playbackRate(
  291. videoFroms.defaultSpeed
  292. );
  293. }
  294. }}></i>
  295. <NSlider
  296. value={videoFroms.defaultSpeed}
  297. step={0.1}
  298. max={1.5}
  299. min={0.5}
  300. vertical
  301. tooltip={false}
  302. onUpdate:value={(val: number) => {
  303. videoFroms.defaultSpeed = val;
  304. if (videoItem.value) {
  305. videoItem.value.playbackRate(
  306. videoFroms.defaultSpeed
  307. );
  308. }
  309. }}>
  310. {{
  311. thumb: () => (
  312. <div class={styles.sliderPoint}>
  313. {videoFroms.defaultSpeed}
  314. <span>x</span>
  315. </div>
  316. )
  317. }}
  318. </NSlider>
  319. <i
  320. class={[styles.iconCut]}
  321. onClick={() => {
  322. if (videoFroms.defaultSpeed <= 0.5) {
  323. return;
  324. }
  325. if (videoItem.value) {
  326. videoFroms.defaultSpeed =
  327. (videoFroms.defaultSpeed * 10 - 1) / 10;
  328. videoItem.value.playbackRate(
  329. videoFroms.defaultSpeed
  330. );
  331. }
  332. }}></i>
  333. </div>
  334. </div>
  335. </div>
  336. </div>
  337. </div>
  338. <div class={styles.slider}>
  339. <NSlider
  340. value={videoFroms.currentTimeNum}
  341. step={0.01}
  342. max={videoFroms.durationNum}
  343. tooltip={false}
  344. onUpdate:value={(val: number) => {
  345. videoFroms.speedControl = false;
  346. videoItem.value.currentTime(val);
  347. videoFroms.currentTimeNum = val;
  348. videoFroms.currentTime = timeFormat(Math.round(val || 0));
  349. }}
  350. />
  351. </div>
  352. <div class={styles.actions}>
  353. <div class={styles.time}>
  354. <div
  355. class="plyr__time plyr__time--current"
  356. aria-label="Current time">
  357. {videoFroms.currentTime}
  358. </div>
  359. <span class={styles.line}>/</span>
  360. <div
  361. class="plyr__time plyr__time--duration"
  362. aria-label="Duration">
  363. {videoFroms.duration}
  364. </div>
  365. </div>
  366. <div class={styles.actionWrap}>
  367. {props.isDownload && (
  368. <button class={styles.iconDownload} onClick={onDownload}>
  369. <img src={iconPreviewDownload} />
  370. </button>
  371. )}
  372. {props.fullscreen && (
  373. <button class={styles.iconDownload} onClick={onFullScreen}>
  374. <img
  375. src={
  376. videoFroms.isFullScreen
  377. ? iconFullscreenExit
  378. : iconFullscreen
  379. }
  380. />
  381. </button>
  382. )}
  383. </div>
  384. </div>
  385. </div>
  386. </div>
  387. </div>
  388. );
  389. }
  390. });