index.vue 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. <template>
  2. <div class="video-controls-wrap">
  3. <div class="left">
  4. <div
  5. class="play"
  6. @click="appStore.playing = !appStore.playing"
  7. >
  8. <n-icon size="25">
  9. <Pause v-if="appStore.playing"></Pause>
  10. <Play v-else></Play>
  11. </n-icon>
  12. </div>
  13. <div
  14. class="refresh"
  15. @click="debounceRefresh"
  16. >
  17. <n-icon size="25">
  18. <RefreshSharp></RefreshSharp>
  19. </n-icon>
  20. </div>
  21. <div
  22. class="muted"
  23. @click="cacheStore.setMuted(!cacheStore.muted)"
  24. >
  25. <n-popover
  26. placement="top"
  27. trigger="hover"
  28. :flip="false"
  29. :style="{ padding: '4px 4px 24px 4px' }"
  30. :show-arrow="false"
  31. >
  32. <template #trigger>
  33. <n-icon size="25">
  34. <VolumeMuteOutline v-if="cacheStore.muted"></VolumeMuteOutline>
  35. <VolumeHighOutline v-else></VolumeHighOutline>
  36. </n-icon>
  37. </template>
  38. <div class="slider">
  39. <div class="txt">{{ cacheStore.volume }}</div>
  40. <n-slider
  41. :value="cacheStore.volume"
  42. :step="1"
  43. vertical
  44. :tooltip="false"
  45. @update-value="(v) => cacheStore.setVolume(v)"
  46. />
  47. </div>
  48. </n-popover>
  49. </div>
  50. </div>
  51. <div class="right">
  52. <div
  53. class="item fps"
  54. v-if="props.control?.fps && appStore.videoFps"
  55. >
  56. {{ appStore.videoFps }}帧
  57. </div>
  58. <div
  59. class="item kbs"
  60. v-if="props.control?.kbs && appStore.videoKBs"
  61. >
  62. {{ appStore.videoKBs }}KB/s
  63. </div>
  64. <div
  65. class="item resolution"
  66. v-if="props.control?.resolution && resolution"
  67. >
  68. {{ resolution }}
  69. </div>
  70. <div
  71. class="item line"
  72. v-if="props.control?.line"
  73. >
  74. <Dropdown
  75. :positon="'center'"
  76. :is-top="true"
  77. >
  78. <template #btn>
  79. <div class="btn">线路</div>
  80. </template>
  81. <template #list>
  82. <div class="list">
  83. <div
  84. class="iten"
  85. :class="{ active: appStore.liveLine === item }"
  86. v-for="item in LiveLineEnum"
  87. :key="item"
  88. @click="changeLiveLine(item)"
  89. >
  90. {{ item }}
  91. </div>
  92. </div>
  93. </template>
  94. </Dropdown>
  95. </div>
  96. <div
  97. class="item speed"
  98. v-if="props.control?.speed"
  99. >
  100. <Dropdown
  101. :positon="'center'"
  102. :is-top="true"
  103. >
  104. <template #btn>
  105. <div class="btn">倍速</div>
  106. </template>
  107. <template #list>
  108. <div
  109. class="list"
  110. @click="handleTip"
  111. >
  112. <div class="iten">2.0x</div>
  113. <div class="iten">1.5x</div>
  114. <div class="iten active">1.0x</div>
  115. <div class="iten">0.5x</div>
  116. </div>
  117. </template>
  118. </Dropdown>
  119. </div>
  120. <div
  121. class="item render"
  122. v-if="props.control?.renderMode"
  123. >
  124. <Dropdown
  125. :positon="'center'"
  126. :is-top="true"
  127. >
  128. <template #btn>
  129. <div class="btn">渲染模式</div>
  130. </template>
  131. <template #list>
  132. <div class="list">
  133. <div
  134. class="iten"
  135. :class="{ active: appStore.videoControls?.renderMode === item }"
  136. v-for="item in LiveRenderEnum"
  137. :key="item"
  138. @click="appStore.videoControls.renderMode = item"
  139. >
  140. {{ item }}
  141. </div>
  142. </div>
  143. </template>
  144. </Dropdown>
  145. </div>
  146. <div
  147. class="item"
  148. v-if="props.control?.pipMode"
  149. >
  150. <span
  151. class="txt"
  152. @click="handlePip"
  153. >
  154. {{
  155. !appStore.videoControlsValue.pipMode ? '开启画中画' : '退出画中画'
  156. }}
  157. </span>
  158. </div>
  159. <div
  160. class="item"
  161. v-if="props.control?.pageFullMode"
  162. >
  163. <span
  164. class="txt"
  165. @click="handlePageFull"
  166. >
  167. {{
  168. !appStore.videoControlsValue.pageFullMode
  169. ? '开启网页全屏'
  170. : '退出网页全屏'
  171. }}
  172. </span>
  173. </div>
  174. <div
  175. class="item"
  176. v-if="props.control?.fullMode"
  177. >
  178. <span
  179. class="txt"
  180. @click="emits('fullScreen')"
  181. >
  182. 全屏
  183. </span>
  184. </div>
  185. </div>
  186. </div>
  187. </template>
  188. <script lang="ts" setup>
  189. import {
  190. Pause,
  191. Play,
  192. RefreshSharp,
  193. VolumeHighOutline,
  194. VolumeMuteOutline,
  195. } from '@vicons/ionicons5';
  196. import { debounce, isSafari } from 'billd-utils';
  197. import { onMounted, onUnmounted } from 'vue';
  198. import { handleTip } from '@/hooks/use-common';
  199. import { LiveLineEnum, LiveRenderEnum } from '@/interface';
  200. import { AppRootState, useAppStore } from '@/store/app';
  201. import { usePiniaCacheStore } from '@/store/cache';
  202. import { LiveRoomTypeEnum } from '@/types/ILiveRoom';
  203. const props = withDefaults(
  204. defineProps<{
  205. resolution?: string;
  206. control?: AppRootState['videoControls'];
  207. }>(),
  208. {}
  209. );
  210. const emits = defineEmits([
  211. 'refresh',
  212. 'fullScreen',
  213. 'pageFullScreen',
  214. 'pictureInPicture',
  215. ]);
  216. const cacheStore = usePiniaCacheStore();
  217. const appStore = useAppStore();
  218. const debounceRefresh = debounce(() => {
  219. emits('refresh');
  220. }, 500);
  221. onMounted(() => {
  222. window.addEventListener('keydown', handleKeydown);
  223. });
  224. onUnmounted(() => {
  225. window.removeEventListener('keydown', handleKeydown);
  226. });
  227. function handleKeydown(e) {
  228. if (e.key === 'Escape') {
  229. console.log('esc');
  230. if (appStore.videoControlsValue.pageFullMode) {
  231. window.$message.info('已退出网页全屏');
  232. appStore.videoControlsValue.pageFullMode = false;
  233. }
  234. }
  235. }
  236. function handlePip() {
  237. if (
  238. isSafari() &&
  239. appStore.videoControls.renderMode === LiveRenderEnum.canvas
  240. ) {
  241. window.$message.info('请先切换渲染模式为video');
  242. return;
  243. }
  244. emits('pictureInPicture');
  245. appStore.videoControlsValue.pipMode = !appStore.videoControlsValue.pipMode;
  246. }
  247. function handlePageFull() {
  248. emits('pageFullScreen');
  249. if (!appStore.videoControlsValue.pageFullMode) {
  250. window.$message.info('按esc可快速退出网页全屏');
  251. }
  252. appStore.videoControlsValue.pageFullMode =
  253. !appStore.videoControlsValue.pageFullMode;
  254. }
  255. function changeLiveLine(item: LiveLineEnum) {
  256. if (
  257. [
  258. LiveRoomTypeEnum.wertc_live,
  259. LiveRoomTypeEnum.wertc_meeting_one,
  260. LiveRoomTypeEnum.wertc_meeting_two,
  261. ].includes(appStore.liveRoomInfo?.type!) &&
  262. item !== LiveLineEnum.rtc
  263. ) {
  264. window.$message.info('不支持该线路!');
  265. return;
  266. } else if (
  267. ![
  268. LiveRoomTypeEnum.wertc_live,
  269. LiveRoomTypeEnum.wertc_meeting_one,
  270. LiveRoomTypeEnum.wertc_meeting_two,
  271. ].includes(appStore.liveRoomInfo?.type!) &&
  272. item === LiveLineEnum.rtc
  273. ) {
  274. window.$message.info('不支持该线路!');
  275. return;
  276. }
  277. appStore.setLiveLine(item);
  278. }
  279. </script>
  280. <style lang="scss" scoped>
  281. .slider {
  282. display: flex;
  283. flex-wrap: wrap;
  284. justify-content: center;
  285. width: 24px;
  286. height: 80px;
  287. text-align: center;
  288. .txt {
  289. font-size: 12px;
  290. }
  291. }
  292. .video-controls-wrap {
  293. position: absolute;
  294. bottom: 0;
  295. left: 0;
  296. z-index: 50;
  297. display: flex;
  298. align-items: center;
  299. justify-content: space-between;
  300. box-sizing: border-box;
  301. padding: 0 20px 0 10px;
  302. width: 100%;
  303. height: 40px;
  304. background-image: linear-gradient(
  305. -180deg,
  306. rgba(0, 0, 0, 0),
  307. rgba(0, 0, 0, 0.7)
  308. );
  309. color: white;
  310. text-align: initial;
  311. user-select: none;
  312. .left {
  313. display: flex;
  314. align-items: center;
  315. .muted,
  316. .refresh,
  317. .play {
  318. display: flex;
  319. align-items: center;
  320. margin-right: 10px;
  321. cursor: pointer;
  322. }
  323. }
  324. .right {
  325. display: flex;
  326. align-items: center;
  327. .item {
  328. position: relative;
  329. margin-right: 15px;
  330. cursor: pointer;
  331. &.fps,
  332. &.kbs,
  333. &.resolution {
  334. cursor: no-drop;
  335. }
  336. }
  337. .render,
  338. .resolution,
  339. .line,
  340. .speed,
  341. .full {
  342. &:hover {
  343. .list {
  344. display: block;
  345. }
  346. }
  347. :deep(.wrap) {
  348. border-radius: 0px;
  349. background-color: rgba($color: #000000, $alpha: 0.5);
  350. color: white;
  351. }
  352. .list {
  353. text-align: center;
  354. .iten {
  355. padding: 6px 10px;
  356. &.active {
  357. color: $theme-color-gold;
  358. }
  359. &:not(:last-child) {
  360. margin-bottom: 4px;
  361. }
  362. &:hover {
  363. background-color: rgba($color: #ffffff, $alpha: 0.1);
  364. cursor: pointer;
  365. }
  366. }
  367. }
  368. }
  369. & > :last-child {
  370. margin-right: 0px;
  371. }
  372. }
  373. }
  374. </style>