index.tsx 12 KB

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