VideoPlayer.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  1. <template>
  2. <section class="space-y-6">
  3. <!-- 返回按钮 -->
  4. <div class="flex items-center gap-3">
  5. <button
  6. @click="goBack"
  7. class="flex items-center gap-2 text-white/80 hover:text-white transition"
  8. >
  9. <svg
  10. class="w-5 h-5"
  11. fill="none"
  12. stroke="currentColor"
  13. viewBox="0 0 24 24"
  14. >
  15. <path
  16. stroke-linecap="round"
  17. stroke-linejoin="round"
  18. stroke-width="2"
  19. d="M15 19l-7-7 7-7"
  20. />
  21. </svg>
  22. <span class="text-sm">返回</span>
  23. </button>
  24. <div class="relative">
  25. <button
  26. @click="toggleShare"
  27. class="p-2 rounded-lg bg-white/5 border border-white/10 text-white/70 hover:bg-white/10 hover:text-white transition"
  28. >
  29. <svg
  30. class="w-5 h-5"
  31. fill="none"
  32. stroke="currentColor"
  33. viewBox="0 0 24 24"
  34. >
  35. <path
  36. stroke-linecap="round"
  37. stroke-linejoin="round"
  38. stroke-width="2"
  39. d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z"
  40. />
  41. </svg>
  42. </button>
  43. <!-- 分享提示弹窗 -->
  44. <div
  45. v-if="showShareModal"
  46. class="absolute top-0 left-full ml-2 z-50 bg-emerald-500 text-white px-3 py-1.5 rounded-lg shadow-lg flex items-center gap-1.5 whitespace-nowrap"
  47. >
  48. <svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
  49. <path
  50. fill-rule="evenodd"
  51. d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
  52. clip-rule="evenodd"
  53. />
  54. </svg>
  55. <span class="text-xs font-medium">已复制</span>
  56. </div>
  57. </div>
  58. </div>
  59. <!-- 视频播放器区域 -->
  60. <div class="relative rounded-2xl overflow-hidden bg-black">
  61. <div class="aspect-video video-container">
  62. <video
  63. ref="videoPlayer"
  64. :poster="videoInfo.cover"
  65. class="w-full h-full object-contain"
  66. controls
  67. preload="metadata"
  68. playsinline
  69. webkit-playsinline
  70. x5-playsinline
  71. x5-video-player-type="h5"
  72. x5-video-player-fullscreen="true"
  73. x5-video-orientation="landscape"
  74. @loadstart="onVideoLoadStart"
  75. @loadeddata="onVideoLoadedData"
  76. @error="onVideoError"
  77. @canplay="onVideoCanPlay"
  78. >
  79. 您的浏览器不支持视频播放
  80. </video>
  81. <!-- 视频错误提示 -->
  82. <div
  83. v-if="videoError"
  84. class="absolute inset-0 flex items-center justify-center bg-black/80 text-white"
  85. >
  86. <div class="text-center p-6">
  87. <div class="text-4xl mb-4">⚠️</div>
  88. <h3 class="text-lg font-semibold mb-2">视频加载失败</h3>
  89. <p class="text-sm text-white/70 mb-4">{{ videoError }}</p>
  90. <button
  91. v-if="showRetryButton"
  92. @click="retryVideoLoad"
  93. :disabled="!canRetry"
  94. class="px-4 py-2 rounded-lg transition"
  95. :class="
  96. canRetry
  97. ? 'bg-emerald-500 text-white hover:bg-emerald-600'
  98. : 'bg-gray-500 text-gray-300 cursor-not-allowed'
  99. "
  100. >
  101. 重试
  102. </button>
  103. </div>
  104. </div>
  105. </div>
  106. </div>
  107. <!-- 视频信息区域 -->
  108. <div class="space-y-6">
  109. <!-- 视频标题和基本信息 -->
  110. <div class="space-y-3">
  111. <h1 class="text-xl font-semibold text-white leading-tight">
  112. {{ videoInfo.name || "视频标题" }}
  113. </h1>
  114. <div class="flex items-center gap-4 text-sm text-white/60">
  115. <div class="flex items-center gap-1">
  116. <svg
  117. class="w-4 h-4"
  118. fill="none"
  119. stroke="currentColor"
  120. viewBox="0 0 24 24"
  121. >
  122. <path
  123. stroke-linecap="round"
  124. stroke-linejoin="round"
  125. stroke-width="2"
  126. d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
  127. />
  128. </svg>
  129. <span>{{ formatDuration(videoInfo.duration) }}</span>
  130. </div>
  131. <div class="flex items-center gap-1">
  132. <svg
  133. class="w-4 h-4"
  134. fill="none"
  135. stroke="currentColor"
  136. viewBox="0 0 24 24"
  137. >
  138. <path
  139. stroke-linecap="round"
  140. stroke-linejoin="round"
  141. stroke-width="2"
  142. d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
  143. />
  144. <path
  145. stroke-linecap="round"
  146. stroke-linejoin="round"
  147. stroke-width="2"
  148. d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
  149. />
  150. </svg>
  151. <span>{{ formatNumber(videoInfo.view) }} 次观看</span>
  152. </div>
  153. <div class="flex items-center gap-1">
  154. <svg
  155. class="w-4 h-4"
  156. fill="none"
  157. stroke="currentColor"
  158. viewBox="0 0 24 24"
  159. >
  160. <path
  161. stroke-linecap="round"
  162. stroke-linejoin="round"
  163. stroke-width="2"
  164. d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
  165. />
  166. </svg>
  167. <span>{{ formatNumber(videoInfo.like) }} 点赞</span>
  168. </div>
  169. </div>
  170. </div>
  171. <!-- 标签信息 -->
  172. <div
  173. v-if="videoInfo.taginfo && videoInfo.taginfo.length > 0"
  174. class="space-y-3"
  175. >
  176. <h3 class="text-sm font-medium text-white/80">标签</h3>
  177. <div class="flex flex-wrap gap-2">
  178. <span
  179. v-for="tag in videoInfo.taginfo"
  180. :key="tag.hash"
  181. class="px-3 py-1.5 rounded-full bg-white/5 border border-white/10 text-xs text-white/70 hover:bg-white/10 hover:text-white transition"
  182. >
  183. {{ tag.name }}
  184. </span>
  185. </div>
  186. </div>
  187. <!-- 相关推荐 -->
  188. <div v-if="relatedVideos.length > 0" class="space-y-4">
  189. <h3 class="text-sm font-medium text-white/80">相关推荐</h3>
  190. <div class="grid grid-cols-2 md:grid-cols-3 gap-2">
  191. <article
  192. v-for="video in relatedVideos.slice(0, 15)"
  193. :key="video.id"
  194. @click="playVideo(video)"
  195. class="group rounded-xl overflow-hidden bg-white/5 border border-white/10 cursor-pointer hover:bg-white/10 transition"
  196. >
  197. <div class="aspect-[9/12] relative">
  198. <img
  199. :src="video.cover"
  200. :alt="video.name"
  201. class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
  202. @error="handleImageError"
  203. />
  204. <div
  205. class="absolute bottom-1 right-1 bg-black/70 text-white text-xs px-1.5 py-0.5 rounded"
  206. >
  207. {{ formatDuration(video.duration) }}
  208. </div>
  209. </div>
  210. <div class="p-2">
  211. <h4
  212. class="text-xs font-medium text-white/90 leading-tight line-clamp-2"
  213. >
  214. {{ video.name }}
  215. </h4>
  216. <p class="text-xs text-white/50 mt-0.5">
  217. {{ formatNumber(video.view) }} 观看
  218. </p>
  219. </div>
  220. </article>
  221. </div>
  222. </div>
  223. </div>
  224. </section>
  225. </template>
  226. <script setup lang="ts">
  227. import { ref, onMounted, onUnmounted, computed } from "vue";
  228. import { useRoute, useRouter } from "vue-router";
  229. import { searchVideoByTags } from "@/services/api";
  230. import Hls from "hls.js";
  231. // 路由相关
  232. const route = useRoute();
  233. const router = useRouter();
  234. // 视频播放器引用
  235. const videoPlayer = ref<HTMLVideoElement>();
  236. // HLS实例
  237. let hls: Hls | null = null;
  238. // 视频信息
  239. const videoInfo = ref<any>({
  240. id: "",
  241. name: "",
  242. cover: "",
  243. m3u8: "",
  244. duration: 0,
  245. view: 0,
  246. like: 0,
  247. time: 0,
  248. taginfo: [],
  249. });
  250. // 相关视频
  251. const relatedVideos = ref<any[]>([]);
  252. // 状态管理
  253. const showShareModal = ref(false);
  254. const videoError = ref<string>("");
  255. const retryCount = ref(0);
  256. const maxRetries = 3;
  257. const lastRetryTime = ref(0);
  258. const retryCooldown = 3000; // 3秒冷却时间
  259. const forceUpdate = ref(0); // 强制更新触发器
  260. // 生成设备标识
  261. const generateMacAddress = (): string => {
  262. const hex = "0123456789ABCDEF";
  263. let mac = "";
  264. for (let i = 0; i < 6; i++) {
  265. if (i > 0) mac += ":";
  266. mac += hex[Math.floor(Math.random() * 16)];
  267. mac += hex[Math.floor(Math.random() * 16)];
  268. }
  269. return mac;
  270. };
  271. const device = generateMacAddress();
  272. // 计算属性
  273. const canRetry = computed(() => {
  274. forceUpdate.value; // 依赖forceUpdate来触发重新计算
  275. const now = Date.now();
  276. const isCooldownActive = now - lastRetryTime.value < retryCooldown;
  277. const hasRetriesLeft = retryCount.value < maxRetries;
  278. return !isCooldownActive && hasRetriesLeft;
  279. });
  280. const showRetryButton = computed(() => {
  281. return retryCount.value < maxRetries;
  282. });
  283. // 格式化时长
  284. const formatDuration = (duration: string | number): string => {
  285. const seconds = parseInt(String(duration));
  286. const minutes = Math.floor(seconds / 60);
  287. const remainingSeconds = seconds % 60;
  288. return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
  289. };
  290. // 格式化数字
  291. const formatNumber = (num: string | number): string => {
  292. const n = parseInt(String(num));
  293. if (n >= 10000) {
  294. return `${(n / 10000).toFixed(1)}万`;
  295. }
  296. return n.toString();
  297. };
  298. // 处理图片加载错误
  299. const handleImageError = (event: Event) => {
  300. const img = event.target as HTMLImageElement;
  301. img.src =
  302. "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSIjMzMzIi8+CjxwYXRoIGQ9Ik0xMDAgNzVMMTI1IDEwMEgxMDBWMTI1SDc1TDEwMCAxMDBaIiBmaWxsPSIjNjY2Ii8+Cjwvc3ZnPgo=";
  303. };
  304. // 视频播放器事件处理
  305. const onVideoLoadStart = () => {
  306. console.log("视频开始加载...");
  307. };
  308. const onVideoLoadedData = () => {
  309. console.log("视频数据加载完成");
  310. };
  311. const onVideoCanPlay = () => {
  312. console.log("视频可以播放");
  313. };
  314. const onVideoError = (event: Event) => {
  315. const video = event.target as HTMLVideoElement;
  316. console.error("视频播放错误:", video.error);
  317. console.error("错误详情:", {
  318. code: video.error?.code,
  319. message: video.error?.message,
  320. networkState: video.networkState,
  321. readyState: video.readyState,
  322. });
  323. // 设置统一的错误信息
  324. videoError.value = "视频加载失败";
  325. };
  326. // 重试视频加载
  327. const retryVideoLoad = () => {
  328. const now = Date.now();
  329. // 检查是否在冷却时间内
  330. if (now - lastRetryTime.value < retryCooldown) {
  331. const remainingTime = Math.ceil(
  332. (retryCooldown - (now - lastRetryTime.value)) / 1000
  333. );
  334. console.log(`重试冷却中,还需等待 ${remainingTime} 秒`);
  335. return;
  336. }
  337. // 检查是否超过最大重试次数
  338. if (retryCount.value >= maxRetries) {
  339. console.log("已达到最大重试次数");
  340. return;
  341. }
  342. // 更新重试状态
  343. retryCount.value++;
  344. lastRetryTime.value = now;
  345. console.log(`第 ${retryCount.value} 次重试`);
  346. videoError.value = "";
  347. destroyHls();
  348. // 延迟重新初始化
  349. setTimeout(() => {
  350. initHlsPlayer();
  351. }, 500);
  352. };
  353. // 初始化HLS播放器
  354. const initHlsPlayer = () => {
  355. if (!videoPlayer.value) return;
  356. const video = videoPlayer.value;
  357. const videoSrc = videoInfo.value.m3u8;
  358. if (!videoSrc) {
  359. console.error("没有视频源地址");
  360. return;
  361. }
  362. // console.log("初始化HLS播放器,视频源:", videoSrc);
  363. console.log("初始化HLS播放器");
  364. // 检查浏览器是否原生支持HLS
  365. if (video.canPlayType("application/vnd.apple.mpegurl")) {
  366. // Safari原生支持HLS
  367. console.log("使用原生HLS支持");
  368. video.src = videoSrc;
  369. } else if (Hls.isSupported()) {
  370. // 使用HLS.js
  371. console.log("使用HLS.js支持");
  372. // 先检测URL是否为HLS格式
  373. detectVideoFormat(videoSrc)
  374. .then((format) => {
  375. if (format === "hls") {
  376. initHlsJsPlayer(videoSrc, video);
  377. } else {
  378. console.log("检测到非HLS格式,尝试直接播放");
  379. video.src = videoSrc;
  380. }
  381. })
  382. .catch((error) => {
  383. console.error("格式检测失败,尝试直接播放:", error);
  384. video.src = videoSrc;
  385. });
  386. } else {
  387. console.error("浏览器不支持HLS播放");
  388. video.src = videoSrc;
  389. }
  390. };
  391. // 检测视频格式
  392. const detectVideoFormat = async (url: string): Promise<"hls" | "direct"> => {
  393. try {
  394. const response = await fetch(url, {
  395. method: "HEAD",
  396. mode: "cors",
  397. });
  398. const contentType = response.headers.get("content-type") || "";
  399. console.log("Content-Type:", contentType);
  400. // 检查是否为HLS格式
  401. if (
  402. contentType.includes("application/vnd.apple.mpegurl") ||
  403. contentType.includes("application/x-mpegURL") ||
  404. url.includes(".m3u8")
  405. ) {
  406. return "hls";
  407. }
  408. // 检查是否为直接视频文件
  409. if (contentType.includes("video/")) {
  410. return "direct";
  411. }
  412. // 如果无法确定,尝试获取前几个字节来判断
  413. const textResponse = await fetch(url, {
  414. method: "GET",
  415. mode: "cors",
  416. headers: { Range: "bytes=0-1023" },
  417. });
  418. const text = await textResponse.text();
  419. if (text.startsWith("#EXTM3U")) {
  420. return "hls";
  421. }
  422. return "direct";
  423. } catch (error) {
  424. console.error("格式检测失败:", error);
  425. return "direct";
  426. }
  427. };
  428. // 初始化HLS.js播放器
  429. const initHlsJsPlayer = (videoSrc: string, video: HTMLVideoElement) => {
  430. hls = new Hls({
  431. debug: false, // 关闭调试日志
  432. enableWorker: true,
  433. lowLatencyMode: true,
  434. });
  435. hls.loadSource(videoSrc);
  436. hls.attachMedia(video);
  437. hls.on(Hls.Events.MANIFEST_PARSED, () => {
  438. console.log("HLS清单解析完成,可以播放");
  439. });
  440. hls.on(Hls.Events.ERROR, (event, data) => {
  441. console.error("HLS错误:", data);
  442. if (data.fatal) {
  443. switch (data.type) {
  444. case Hls.ErrorTypes.NETWORK_ERROR:
  445. console.error("网络错误,尝试恢复...");
  446. if (data.details === "manifestParsingError") {
  447. videoError.value = "视频加载失败";
  448. // 销毁HLS实例,尝试直接播放
  449. hls?.destroy();
  450. hls = null;
  451. video.src = videoSrc;
  452. } else {
  453. hls?.startLoad();
  454. }
  455. break;
  456. case Hls.ErrorTypes.MEDIA_ERROR:
  457. console.error("媒体错误,尝试恢复...");
  458. hls?.recoverMediaError();
  459. break;
  460. default:
  461. console.error("无法恢复的错误,尝试直接播放");
  462. videoError.value = "视频加载失败";
  463. // 销毁HLS实例,尝试直接播放
  464. hls?.destroy();
  465. hls = null;
  466. video.src = videoSrc;
  467. break;
  468. }
  469. }
  470. });
  471. };
  472. // 清理HLS实例
  473. const destroyHls = () => {
  474. if (hls) {
  475. console.log("清理HLS实例");
  476. hls.destroy();
  477. hls = null;
  478. }
  479. };
  480. // 返回上一页
  481. const goBack = () => {
  482. router.back();
  483. };
  484. // 分享视频(直接复制链接并显示提示)
  485. const toggleShare = async () => {
  486. try {
  487. const videoUrl = window.location.href;
  488. await navigator.clipboard.writeText(videoUrl);
  489. showShareModal.value = true;
  490. // 1秒后自动隐藏提示
  491. setTimeout(() => {
  492. showShareModal.value = false;
  493. }, 1000);
  494. } catch (error) {
  495. console.error("复制链接失败:", error);
  496. }
  497. };
  498. // 播放视频
  499. const playVideo = (video: any) => {
  500. router.push({
  501. name: "VideoPlayer",
  502. params: { id: video.id },
  503. query: {
  504. name: video.name,
  505. cover: video.cover,
  506. m3u8: video.m3u8,
  507. duration: video.duration,
  508. view: video.view,
  509. like: video.like,
  510. time: video.time,
  511. taginfo: JSON.stringify(video.taginfo || []),
  512. },
  513. });
  514. };
  515. // 加载视频信息
  516. const loadVideoInfo = () => {
  517. // 优先从路由参数获取ID
  518. const videoId = route.params.id || route.query.id;
  519. // 从查询参数获取其他信息
  520. const videoData = route.query;
  521. if (videoId) {
  522. videoInfo.value = {
  523. id: videoId,
  524. name: videoData.name || `视频 ${videoId}`,
  525. cover: videoData.cover || "",
  526. m3u8: "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8",
  527. duration: videoData.duration || 0,
  528. view: videoData.view || 0,
  529. like: videoData.like || 0,
  530. time: videoData.time || 0,
  531. taginfo: videoData.taginfo ? JSON.parse(videoData.taginfo as string) : [],
  532. };
  533. // 设置页面标题
  534. document.title = `${videoInfo.value.name} - 视频播放`;
  535. // console.log("加载视频信息:", videoInfo.value);
  536. } else {
  537. console.error("未找到视频ID");
  538. }
  539. };
  540. // 加载相关视频
  541. const loadRelatedVideos = async () => {
  542. try {
  543. // 获取当前视频的标签
  544. const currentVideoTags = videoInfo.value.taginfo || [];
  545. if (currentVideoTags.length > 0) {
  546. // 随机选择一个标签
  547. const randomTag =
  548. currentVideoTags[Math.floor(Math.random() * currentVideoTags.length)];
  549. // 生成1-10之间的随机页码
  550. const randomPage = Math.floor(Math.random() * 10) + 1;
  551. // 获取15个该标签下的最热视频
  552. const response = await searchVideoByTags(
  553. device,
  554. randomPage,
  555. 15,
  556. randomTag.hash,
  557. "long",
  558. "view"
  559. );
  560. if (response.status === 0 && response.data?.list) {
  561. // 过滤掉当前视频
  562. relatedVideos.value = response.data.list
  563. .filter((video: any) => video.id !== videoInfo.value.id)
  564. .slice(0, 15);
  565. }
  566. } else {
  567. // 如果没有标签,则获取全局最热视频
  568. const randomPage = Math.floor(Math.random() * 10) + 1;
  569. const response = await searchVideoByTags(
  570. device,
  571. randomPage,
  572. 15,
  573. undefined,
  574. "long",
  575. "view"
  576. );
  577. if (response.status === 0 && response.data?.list) {
  578. relatedVideos.value = response.data.list
  579. .filter((video: any) => video.id !== videoInfo.value.id)
  580. .slice(0, 15);
  581. }
  582. }
  583. } catch (error) {
  584. console.error("加载相关视频失败:", error);
  585. }
  586. };
  587. onMounted(() => {
  588. loadVideoInfo();
  589. loadRelatedVideos();
  590. // 延迟初始化HLS播放器,确保DOM已渲染
  591. setTimeout(() => {
  592. initHlsPlayer();
  593. }, 100);
  594. // 启动定时器更新按钮状态
  595. const timer = setInterval(() => {
  596. if (videoError.value && retryCount.value < maxRetries) {
  597. forceUpdate.value++; // 触发计算属性重新计算
  598. }
  599. }, 1000);
  600. // 组件卸载时清除定时器
  601. onUnmounted(() => {
  602. clearInterval(timer);
  603. });
  604. });
  605. onUnmounted(() => {
  606. destroyHls();
  607. });
  608. </script>
  609. <style scoped>
  610. /* 视频容器样式 */
  611. .video-container {
  612. position: relative;
  613. }
  614. /* 移动端全屏样式 */
  615. @media screen and (max-width: 768px) {
  616. video {
  617. /* 强制横屏全屏 */
  618. object-fit: contain;
  619. }
  620. /* 全屏时的样式 */
  621. video:fullscreen {
  622. width: 100vw;
  623. height: 100vh;
  624. object-fit: contain;
  625. background: black;
  626. }
  627. /* WebKit全屏样式 */
  628. video:-webkit-full-screen {
  629. width: 100vw;
  630. height: 100vh;
  631. object-fit: contain;
  632. background: black;
  633. }
  634. /* Mozilla全屏样式 */
  635. video:-moz-full-screen {
  636. width: 100vw;
  637. height: 100vh;
  638. object-fit: contain;
  639. background: black;
  640. }
  641. /* MS全屏样式 */
  642. video:-ms-fullscreen {
  643. width: 100vw;
  644. height: 100vh;
  645. object-fit: contain;
  646. background: black;
  647. }
  648. }
  649. </style>