message-video.vue 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. <template>
  2. <div class="message-video">
  3. <div
  4. class="message-video-box"
  5. :class="[
  6. !data.progress &&
  7. data.message.status === 'success' &&
  8. isH5 &&
  9. 'message-video-cover'
  10. ]"
  11. @click="toggleShow"
  12. ref="skeleton"
  13. >
  14. <img
  15. class="message-img"
  16. v-if="(data.progress && poster) || (isH5 && poster)"
  17. :class="[isWidth ? 'isWidth' : 'isHeight']"
  18. :src="poster"
  19. />
  20. <video
  21. class="message-img video-h5-uploading"
  22. v-else-if="isH5"
  23. :src="data.url + '#t=0.1'"
  24. :poster="data.url"
  25. preload="auto"
  26. muted
  27. ref="video"
  28. ></video>
  29. <video
  30. class="message-img video-web"
  31. v-else-if="!data.progress && !isH5"
  32. :src="data.url"
  33. controls
  34. preload="metadata"
  35. :poster="poster"
  36. ref="video"
  37. ></video>
  38. <div class="progress" v-if="data.progress">
  39. <progress :value="data.progress" max="1"></progress>
  40. </div>
  41. </div>
  42. <div class="dialog-video" v-if="show && isH5" @click.self="toggleShow">
  43. <header>
  44. <i class="icon icon-close" @click.stop="toggleShow"></i>
  45. </header>
  46. <div
  47. class="dialog-video-box"
  48. :class="[isH5 ? 'dialog-video-h5' : '']"
  49. @click.self="toggleShow"
  50. >
  51. <video
  52. :class="[isWidth ? 'isWidth' : 'isHeight']"
  53. :src="data.url"
  54. controls
  55. autoplay
  56. ></video>
  57. </div>
  58. </div>
  59. </div>
  60. </template>
  61. <script lang="ts">
  62. import {
  63. defineComponent,
  64. watchEffect,
  65. reactive,
  66. toRefs,
  67. computed,
  68. nextTick,
  69. ref,
  70. watch
  71. } from 'vue';
  72. import { handleSkeletonSize } from '../utils/utils';
  73. export default defineComponent({
  74. props: {
  75. data: {
  76. type: Object,
  77. default: () => ({})
  78. },
  79. isH5: {
  80. type: Boolean,
  81. default: false
  82. }
  83. },
  84. setup(props: any, ctx: any) {
  85. const data = reactive({
  86. data: {},
  87. show: false,
  88. poster: '',
  89. posterWidth: 0,
  90. posterHeight: 0
  91. });
  92. const skeleton = ref();
  93. const video = ref();
  94. const isWidth = computed(() => {
  95. // eslint-disable-next-line no-unsafe-optional-chaining
  96. const { snapshotWidth = 0, snapshotHeight = 0 } = (data.data as any)
  97. ?.message?.payload;
  98. return snapshotWidth >= snapshotHeight;
  99. });
  100. const transparentPosterUrl =
  101. 'https://web.sdk.qcloud.com/im/assets/images/transparent.png';
  102. const toggleShow = () => {
  103. if (!(data.data as any).progress) {
  104. data.show = !data.show;
  105. }
  106. };
  107. // h5 部分浏览器(safari / wx)video标签 封面为空 在视频未上传完成前的封面展示需要单独进行处理截取
  108. const getVideoBase64 = (url: string) => {
  109. return new Promise(function (resolve, reject) {
  110. let dataURL = '';
  111. let video = document.createElement('video');
  112. video.setAttribute('crossOrigin', 'anonymous'); //处理跨域
  113. video.setAttribute('src', url);
  114. video.setAttribute('preload', 'auto');
  115. video.addEventListener(
  116. 'loadeddata',
  117. function () {
  118. let canvas = document.createElement('canvas'),
  119. width = video.videoWidth, //canvas的尺寸和图片一样
  120. height = video.videoHeight;
  121. canvas.width = width;
  122. canvas.height = height;
  123. (canvas as any)
  124. .getContext('2d')
  125. .drawImage(video, 0, 0, width, height); //绘制canvas
  126. dataURL = canvas.toDataURL('image/jpeg'); //转换为base64
  127. data.posterWidth = width;
  128. data.posterHeight = height;
  129. resolve(dataURL);
  130. },
  131. { once: true }
  132. );
  133. });
  134. };
  135. const handlePosterUrl = async (data: any) => {
  136. if (!data) return '';
  137. if (data.progress) {
  138. return await getVideoBase64(data.url);
  139. } else {
  140. return (
  141. (data.snapshotUrl !== transparentPosterUrl && data.snapshotUrl) ||
  142. (data?.message?.payload?.snapshotUrl !== transparentPosterUrl &&
  143. data?.message?.payload?.snapshotUrl) ||
  144. (data?.message?.payload?.thumbUrl !== transparentPosterUrl &&
  145. data?.message?.payload?.thumbUrl) ||
  146. (await getVideoBase64(data.url))
  147. );
  148. }
  149. };
  150. watchEffect(async () => {
  151. data.data = props.data;
  152. if (!data.data) return;
  153. data.poster = await handlePosterUrl(data.data);
  154. nextTick(async () => {
  155. const containerWidth =
  156. document.getElementById('messageEle')?.clientWidth || 0;
  157. const max = props.isH5 ? Math.min(containerWidth - 172, 300) : 300;
  158. let size;
  159. if (!(data.data as any).progress) {
  160. let {
  161. snapshotWidth = 0,
  162. snapshotHeight = 0,
  163. snapshotUrl
  164. } = data.data as any;
  165. if (snapshotWidth === 0 || snapshotHeight === 0) return;
  166. if (snapshotUrl === transparentPosterUrl) {
  167. snapshotWidth = data.posterWidth;
  168. snapshotHeight = data.posterHeight;
  169. }
  170. size = handleSkeletonSize(snapshotWidth, snapshotHeight, max, max);
  171. skeleton?.value?.style &&
  172. (skeleton.value.style.width = `${size.width}Px`);
  173. skeleton?.value?.style &&
  174. (skeleton.value.style.height = `${size.height}Px`);
  175. if (!props.isH5) {
  176. video?.value?.style &&
  177. (video.value.style.width = `${size.width}Px`);
  178. video?.value?.style &&
  179. (video.value.style.height = `${size.height}Px`);
  180. }
  181. } else {
  182. ctx.emit('uploading');
  183. }
  184. });
  185. });
  186. watch(
  187. () => (data.data as any)?.progress,
  188. (newVal, oldVal) => {
  189. if (!newVal && oldVal) {
  190. ctx.emit('uploading');
  191. }
  192. }
  193. );
  194. return {
  195. ...toRefs(data),
  196. toggleShow,
  197. isWidth,
  198. getVideoBase64,
  199. handlePosterUrl,
  200. skeleton,
  201. video
  202. };
  203. }
  204. });
  205. </script>
  206. <style lang="scss" scoped>
  207. @import url('../../../styles/common.scss');
  208. @import url('../../../styles/icon.scss');
  209. .message-video {
  210. position: relative;
  211. display: flex;
  212. justify-content: center;
  213. overflow: hidden;
  214. &-box {
  215. max-width: min(calc(100vw - 180Px), 300Px);
  216. video {
  217. max-width: min(calc(100vw - 180Px), 300Px);
  218. max-height: min(calc(100vw - 180Px), 300Px);
  219. width: inherit;
  220. height: inherit;
  221. border-radius: 10Px;
  222. }
  223. img {
  224. max-width: min(calc(100vw - 180Px), 300Px);
  225. max-height: min(calc(100vw - 180Px), 300Px);
  226. width: inherit;
  227. height: inherit;
  228. border-radius: 10Px;
  229. }
  230. img[src=''],
  231. img:not([src]) {
  232. opacity: 0;
  233. }
  234. }
  235. &-cover {
  236. display: inline-block;
  237. position: relative;
  238. &::before {
  239. position: absolute;
  240. z-index: 1;
  241. content: '';
  242. width: 0Px;
  243. height: 0Px;
  244. border: 10Px solid transparent;
  245. border-left: 15Px solid #ffffff;
  246. top: 0;
  247. left: 0;
  248. bottom: 0;
  249. right: 0;
  250. margin: auto;
  251. transform: translate(5Px, 0Px);
  252. }
  253. video {
  254. max-width: min(calc(100vw - 180Px), 300Px);
  255. max-height: min(calc(100vw - 180Px), 300Px);
  256. width: inherit;
  257. height: inherit;
  258. border-radius: 10Px;
  259. }
  260. }
  261. .progress {
  262. position: absolute;
  263. box-sizing: border-box;
  264. width: 100%;
  265. height: 100%;
  266. padding: 0 20Px;
  267. border-radius: 10Px;
  268. left: 0;
  269. top: 0;
  270. background: rgba(#000000, 0.5);
  271. display: flex;
  272. align-items: center;
  273. flex: 1;
  274. progress {
  275. color: #006eff;
  276. appearance: none;
  277. border-radius: 0.25rem;
  278. background: rgba(#ffffff, 1);
  279. width: 100%;
  280. height: 0.5rem;
  281. &::-webkit-progress-value {
  282. background-color: #006eff;
  283. border-radius: 0.25rem;
  284. }
  285. &::-webkit-progress-bar {
  286. border-radius: 0.25rem;
  287. background: rgba(#ffffff, 1);
  288. }
  289. &::-moz-progress-bar {
  290. color: #006eff;
  291. background: #006eff;
  292. border-radius: 0.25rem;
  293. }
  294. }
  295. }
  296. }
  297. .dialog-video {
  298. position: fixed;
  299. z-index: 12;
  300. width: 100vw;
  301. height: 100vh;
  302. top: 0;
  303. left: 0;
  304. display: flex;
  305. flex-direction: column;
  306. align-items: center;
  307. header {
  308. display: flex;
  309. justify-content: flex-end;
  310. background: rgba(0, 0, 0, 0.49);
  311. width: 100%;
  312. box-sizing: border-box;
  313. padding: 10Px 10Px;
  314. }
  315. &-box {
  316. display: flex;
  317. flex: 1;
  318. max-height: 100%;
  319. padding: 6rem;
  320. box-sizing: border-box;
  321. justify-content: center;
  322. align-items: center;
  323. video {
  324. max-width: 100%;
  325. max-height: 100%;
  326. }
  327. }
  328. }
  329. .dialog-video-h5 {
  330. width: 100%;
  331. height: 100%;
  332. background: #000000;
  333. padding: 30Px 0;
  334. }
  335. .isWidth {
  336. width: 100%;
  337. }
  338. .isHeight {
  339. height: 100%;
  340. }
  341. </style>