ViewYoutubeVideo.vue 24 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025
  1. <script setup lang="ts">
  2. import { defineAsyncComponent, onMounted, onBeforeUnmount, ref } from "vue";
  3. import Toast from "toasters";
  4. import { storeToRefs } from "pinia";
  5. import aw from "@/aw";
  6. import { useWebsocketsStore } from "@/stores/websockets";
  7. import { useModalsStore } from "@/stores/modals";
  8. import { useViewYoutubeVideoStore } from "@/stores/viewYoutubeVideo";
  9. import { useStationStore } from "@/stores/station";
  10. import { useUserAuthStore } from "@/stores/userAuth";
  11. import Modal from "@/components/Modal.vue";
  12. const SongThumbnail = defineAsyncComponent(
  13. () => import("@/components/SongThumbnail.vue")
  14. );
  15. const props = defineProps({
  16. modalUuid: { type: String, default: "" }
  17. });
  18. const interval = ref(null);
  19. const loaded = ref(false);
  20. const canvasWidth = ref(760);
  21. const volumeSliderValue = ref(20);
  22. const durationCanvas = ref(null);
  23. const activityWatchVideoDataInterval = ref(null);
  24. const activityWatchVideoLastStatus = ref("");
  25. const activityWatchVideoLastStartDuration = ref(0);
  26. const viewYoutubeVideoStore = useViewYoutubeVideoStore(props);
  27. const stationStore = useStationStore();
  28. const { videoId, youtubeId, video, player } = storeToRefs(
  29. viewYoutubeVideoStore
  30. );
  31. const {
  32. updatePlayer,
  33. stopVideo,
  34. loadVideoById,
  35. pauseVideo,
  36. setPlaybackRate,
  37. viewYoutubeVideo
  38. } = viewYoutubeVideoStore;
  39. const { updateMediaModalPlayingAudio } = stationStore;
  40. const { openModal, closeCurrentModal } = useModalsStore();
  41. const { socket } = useWebsocketsStore();
  42. const userAuthStore = useUserAuthStore();
  43. const { hasPermission } = userAuthStore;
  44. const remove = () => {
  45. socket.dispatch("youtube.removeVideos", videoId.value, res => {
  46. if (res.status === "success") {
  47. new Toast("YouTube video successfully removed.");
  48. closeCurrentModal();
  49. } else {
  50. new Toast("Youtube video with that ID not found.");
  51. }
  52. });
  53. };
  54. const handleConfirmed = ({ action, params }) => {
  55. if (typeof action === "function") {
  56. if (params) action(params);
  57. else action();
  58. }
  59. };
  60. const confirmAction = ({ message, action }) => {
  61. openModal({
  62. modal: "confirm",
  63. data: {
  64. message,
  65. action,
  66. params: null,
  67. onCompleted: handleConfirmed
  68. }
  69. });
  70. };
  71. const seekTo = position => {
  72. pauseVideo(false);
  73. player.value.player.seekTo(position);
  74. };
  75. const settings = type => {
  76. switch (type) {
  77. case "stop":
  78. stopVideo();
  79. pauseVideo(true);
  80. break;
  81. case "pause":
  82. pauseVideo(true);
  83. break;
  84. case "play":
  85. pauseVideo(false);
  86. break;
  87. case "skipToLast10Secs":
  88. seekTo(Number(player.value.duration) - 10);
  89. break;
  90. default:
  91. break;
  92. }
  93. };
  94. const play = () => {
  95. if (player.value.player.getVideoData().video_id !== video.value.youtubeId) {
  96. video.value.duration = -1;
  97. loadVideoById(video.value.youtubeId);
  98. }
  99. settings("play");
  100. };
  101. const changeVolume = () => {
  102. const { volume } = player.value;
  103. localStorage.setItem("volume", `${volume}`);
  104. player.value.player.setVolume(volume);
  105. if (volume > 0) {
  106. player.value.player.unMute();
  107. player.value.muted = false;
  108. }
  109. };
  110. const toggleMute = () => {
  111. const previousVolume = parseFloat(localStorage.getItem("volume"));
  112. const volume = player.value.player.getVolume() <= 0 ? previousVolume : 0;
  113. player.value.muted = !player.value.muted;
  114. volumeSliderValue.value = volume;
  115. player.value.player.setVolume(volume);
  116. if (!player.value.muted) localStorage.setItem("volume", volume.toString());
  117. };
  118. // const increaseVolume = () => {
  119. // const previousVolume = parseFloat(localStorage.getItem("volume"));
  120. // let volume = previousVolume + 5;
  121. // player.value.muted = false;
  122. // if (volume > 100) volume = 100;
  123. // player.value.volume = volume;
  124. // player.value.player.setVolume(volume);
  125. // localStorage.setItem("volume", volume.toString());
  126. // };
  127. const drawCanvas = () => {
  128. if (!loaded.value) return;
  129. const canvasElement = durationCanvas.value;
  130. if (!canvasElement) return;
  131. const ctx = canvasElement.getContext("2d");
  132. const videoDuration = Number(player.value.duration);
  133. const duration = Number(video.value.duration);
  134. const afterDuration = videoDuration - duration;
  135. canvasWidth.value = Math.min(document.body.clientWidth - 40, 760);
  136. const width = canvasWidth.value;
  137. const currentTime =
  138. player.value.player && player.value.player.getCurrentTime
  139. ? player.value.player.getCurrentTime()
  140. : 0;
  141. const widthDuration = (duration / videoDuration) * width;
  142. const widthAfterDuration = (afterDuration / videoDuration) * width;
  143. const widthCurrentTime = (currentTime / videoDuration) * width;
  144. const durationColor = "#03A9F4";
  145. const afterDurationColor = "#41E841";
  146. const currentDurationColor = "#3b25e8";
  147. ctx.fillStyle = durationColor;
  148. ctx.fillRect(0, 0, widthDuration, 20);
  149. ctx.fillStyle = afterDurationColor;
  150. ctx.fillRect(widthDuration, 0, widthAfterDuration, 20);
  151. ctx.fillStyle = currentDurationColor;
  152. ctx.fillRect(widthCurrentTime, 0, 1, 20);
  153. };
  154. const setTrackPosition = event => {
  155. seekTo(
  156. Number(
  157. Number(player.value.player.getDuration()) *
  158. ((event.pageX - event.target.getBoundingClientRect().left) /
  159. canvasWidth.value)
  160. )
  161. );
  162. };
  163. const sendActivityWatchVideoData = () => {
  164. if (!player.value.paused) {
  165. if (activityWatchVideoLastStatus.value !== "playing") {
  166. activityWatchVideoLastStatus.value = "playing";
  167. activityWatchVideoLastStartDuration.value = Math.floor(
  168. Number(player.value.currentTime)
  169. );
  170. }
  171. const videoData = {
  172. title: video.value.title,
  173. artists: video.value.author,
  174. youtubeId: video.value.youtubeId,
  175. muted: player.value.muted,
  176. volume: player.value.volume,
  177. startedDuration:
  178. activityWatchVideoLastStartDuration.value <= 0
  179. ? 0
  180. : activityWatchVideoLastStartDuration.value,
  181. source: `viewYoutubeVideo#${video.value.youtubeId}`,
  182. hostname: window.location.hostname
  183. };
  184. aw.sendVideoData(videoData);
  185. } else {
  186. activityWatchVideoLastStatus.value = "not_playing";
  187. }
  188. };
  189. onMounted(() => {
  190. socket.onConnect(() => {
  191. loaded.value = false;
  192. socket.dispatch(
  193. "youtube.getVideo",
  194. videoId.value || youtubeId.value,
  195. true,
  196. res => {
  197. if (res.status === "success") {
  198. const youtubeVideo = res.data;
  199. viewYoutubeVideo(youtubeVideo);
  200. loaded.value = true;
  201. interval.value = setInterval(() => {
  202. if (
  203. video.value.duration !== -1 &&
  204. player.value.paused === false &&
  205. player.value.playerReady &&
  206. (player.value.player.getCurrentTime() >
  207. video.value.duration ||
  208. (player.value.player.getCurrentTime() > 0 &&
  209. player.value.player.getCurrentTime() >=
  210. player.value.player.getDuration()))
  211. ) {
  212. stopVideo();
  213. pauseVideo(true);
  214. drawCanvas();
  215. }
  216. if (
  217. player.value.playerReady &&
  218. player.value.player.getVideoData &&
  219. player.value.player.getVideoData() &&
  220. player.value.player.getVideoData().video_id ===
  221. video.value.youtubeId
  222. ) {
  223. const currentTime =
  224. player.value.player.getCurrentTime();
  225. if (currentTime !== undefined)
  226. player.value.currentTime =
  227. currentTime.toFixed(3);
  228. if (player.value.duration.indexOf(".000") !== -1) {
  229. const duration =
  230. player.value.player.getDuration();
  231. if (duration !== undefined) {
  232. if (
  233. `${player.value.duration}` ===
  234. `${Number(video.value.duration).toFixed(
  235. 3
  236. )}`
  237. )
  238. video.value.duration =
  239. duration.toFixed(3);
  240. player.value.duration = duration.toFixed(3);
  241. if (
  242. player.value.duration.indexOf(
  243. ".000"
  244. ) !== -1
  245. )
  246. player.value.videoNote = "(~)";
  247. else player.value.videoNote = "";
  248. drawCanvas();
  249. }
  250. }
  251. }
  252. if (player.value.paused === false) drawCanvas();
  253. }, 200);
  254. activityWatchVideoDataInterval.value = setInterval(() => {
  255. sendActivityWatchVideoData();
  256. }, 1000);
  257. if (window.YT && window.YT.Player) {
  258. player.value.player = new window.YT.Player(
  259. `viewYoutubeVideoPlayer-${props.modalUuid}`,
  260. {
  261. height: 298,
  262. width: 530,
  263. videoId: null,
  264. host: "https://www.youtube-nocookie.com",
  265. playerVars: {
  266. controls: 0,
  267. iv_load_policy: 3,
  268. rel: 0,
  269. showinfo: 0,
  270. autoplay: 0
  271. },
  272. events: {
  273. onReady: () => {
  274. let volume = parseFloat(
  275. localStorage.getItem("volume")
  276. );
  277. volume =
  278. typeof volume === "number"
  279. ? volume
  280. : 20;
  281. player.value.player.setVolume(volume);
  282. if (volume > 0)
  283. player.value.player.unMute();
  284. player.value.playerReady = true;
  285. if (video.value && video.value._id)
  286. player.value.player.cueVideoById(
  287. video.value.youtubeId
  288. );
  289. setPlaybackRate(null);
  290. drawCanvas();
  291. },
  292. onStateChange: event => {
  293. drawCanvas();
  294. if (event.data === 1) {
  295. player.value.paused = false;
  296. updateMediaModalPlayingAudio(true);
  297. const youtubeDuration =
  298. player.value.player.getDuration();
  299. const newYoutubeVideoDuration =
  300. youtubeDuration.toFixed(3);
  301. if (
  302. player.value.duration.indexOf(
  303. ".000"
  304. ) !== -1 &&
  305. `${player.value.duration}` !==
  306. `${newYoutubeVideoDuration}`
  307. ) {
  308. const songDurationNumber =
  309. Number(
  310. video.value.duration
  311. );
  312. const songDurationNumber2 =
  313. Number(
  314. video.value.duration
  315. ) + 1;
  316. const songDurationNumber3 =
  317. Number(
  318. video.value.duration
  319. ) - 1;
  320. const fixedSongDuration =
  321. songDurationNumber.toFixed(
  322. 3
  323. );
  324. const fixedSongDuration2 =
  325. songDurationNumber2.toFixed(
  326. 3
  327. );
  328. const fixedSongDuration3 =
  329. songDurationNumber3.toFixed(
  330. 3
  331. );
  332. if (
  333. `${player.value.duration}` ===
  334. `${Number(
  335. video.value.duration
  336. ).toFixed(3)}` &&
  337. (fixedSongDuration ===
  338. player.value.duration ||
  339. fixedSongDuration2 ===
  340. player.value
  341. .duration ||
  342. fixedSongDuration3 ===
  343. player.value
  344. .duration)
  345. )
  346. video.value.duration =
  347. newYoutubeVideoDuration;
  348. player.value.duration =
  349. newYoutubeVideoDuration;
  350. if (
  351. player.value.duration.indexOf(
  352. ".000"
  353. ) !== -1
  354. )
  355. player.value.videoNote =
  356. "(~)";
  357. else
  358. player.value.videoNote = "";
  359. }
  360. if (video.value.duration === -1)
  361. video.value.duration = Number(
  362. player.value.duration
  363. );
  364. if (
  365. video.value.duration >
  366. youtubeDuration + 1
  367. ) {
  368. stopVideo();
  369. pauseVideo(true);
  370. return new Toast(
  371. "Video can't play. Specified duration is bigger than the YouTube song duration."
  372. );
  373. }
  374. if (video.value.duration <= 0) {
  375. stopVideo();
  376. pauseVideo(true);
  377. return new Toast(
  378. "Video can't play. Specified duration has to be more than 0 seconds."
  379. );
  380. }
  381. setPlaybackRate(null);
  382. } else if (event.data === 2) {
  383. player.value.paused = true;
  384. updateMediaModalPlayingAudio(false);
  385. }
  386. return false;
  387. }
  388. }
  389. }
  390. );
  391. } else {
  392. updatePlayer({
  393. error: true,
  394. errorMessage: "Player could not be loaded."
  395. });
  396. }
  397. let volume = parseFloat(localStorage.getItem("volume"));
  398. volume =
  399. typeof volume === "number" && !Number.isNaN(volume)
  400. ? volume
  401. : 20;
  402. localStorage.setItem("volume", volume.toString());
  403. updatePlayer({ volume });
  404. socket.dispatch(
  405. "apis.joinRoom",
  406. `view-youtube-video.${videoId.value}`
  407. );
  408. socket.on(
  409. "event:youtubeVideo.removed",
  410. () => {
  411. new Toast("This YouTube video was removed.");
  412. closeCurrentModal();
  413. },
  414. { modalUuid: props.modalUuid }
  415. );
  416. } else {
  417. new Toast("YouTube video with that ID not found");
  418. closeCurrentModal();
  419. }
  420. }
  421. );
  422. });
  423. });
  424. onBeforeUnmount(() => {
  425. stopVideo();
  426. pauseVideo(true);
  427. updateMediaModalPlayingAudio(false);
  428. player.value.duration = "0.000";
  429. player.value.currentTime = 0;
  430. player.value.playerReady = false;
  431. player.value.videoNote = "";
  432. clearInterval(interval.value);
  433. clearInterval(activityWatchVideoDataInterval.value);
  434. loaded.value = false;
  435. socket.dispatch(
  436. "apis.leaveRoom",
  437. `view-youtube-video.${videoId.value}`,
  438. () => {}
  439. );
  440. // Delete the Pinia store that was created for this modal, after all other cleanup tasks are performed
  441. viewYoutubeVideoStore.$dispose();
  442. });
  443. </script>
  444. <template>
  445. <modal title="View YouTube Video">
  446. <template #body>
  447. <div v-if="loaded" class="top-section">
  448. <div class="left-section">
  449. <p>
  450. <strong>ID:</strong>
  451. <span :title="video._id">{{ video._id }}</span>
  452. </p>
  453. <p>
  454. <strong>YouTube ID:</strong>
  455. <a
  456. :href="
  457. 'https://www.youtube.com/watch?v=' +
  458. `${video.youtubeId}`
  459. "
  460. target="_blank"
  461. >
  462. {{ video.youtubeId }}
  463. </a>
  464. </p>
  465. <p>
  466. <strong>Title:</strong>
  467. <span :title="video.title">{{ video.title }}</span>
  468. </p>
  469. <p>
  470. <strong>Author:</strong>
  471. <span :title="video.author">{{ video.author }}</span>
  472. </p>
  473. <p>
  474. <strong>Duration:</strong>
  475. <span :title="`${video.duration}`">{{
  476. video.duration
  477. }}</span>
  478. </p>
  479. </div>
  480. <div class="right-section">
  481. <song-thumbnail :song="video" class="thumbnail-preview" />
  482. </div>
  483. </div>
  484. <div v-show="loaded" class="player-section">
  485. <div class="player-container">
  486. <div :id="`viewYoutubeVideoPlayer-${modalUuid}`" />
  487. </div>
  488. <div v-show="player.error" class="player-error">
  489. <h2>{{ player.errorMessage }}</h2>
  490. </div>
  491. <canvas
  492. ref="durationCanvas"
  493. class="duration-canvas"
  494. v-show="!player.error"
  495. height="20"
  496. :width="canvasWidth"
  497. @click="setTrackPosition($event)"
  498. />
  499. <div class="player-footer">
  500. <div class="player-footer-left">
  501. <button
  502. class="button is-primary"
  503. @click="play()"
  504. @keyup.enter="play()"
  505. v-if="player.paused"
  506. content="Resume Playback"
  507. v-tippy
  508. >
  509. <i class="material-icons">play_arrow</i>
  510. </button>
  511. <button
  512. class="button is-primary"
  513. @click="settings('pause')"
  514. @keyup.enter="settings('pause')"
  515. v-else
  516. content="Pause Playback"
  517. v-tippy
  518. >
  519. <i class="material-icons">pause</i>
  520. </button>
  521. <button
  522. class="button is-danger"
  523. @click.exact="settings('stop')"
  524. @click.shift="settings('hardStop')"
  525. @keyup.enter.exact="settings('stop')"
  526. @keyup.shift.enter="settings('hardStop')"
  527. content="Stop Playback"
  528. v-tippy
  529. >
  530. <i class="material-icons">stop</i>
  531. </button>
  532. <tippy
  533. class="playerRateDropdown"
  534. :touch="true"
  535. :interactive="true"
  536. placement="bottom"
  537. theme="dropdown"
  538. ref="dropdown"
  539. trigger="click"
  540. append-to="parent"
  541. @show="
  542. () => {
  543. player.showRateDropdown = true;
  544. }
  545. "
  546. @hide="
  547. () => {
  548. player.showRateDropdown = false;
  549. }
  550. "
  551. >
  552. <div
  553. ref="trigger"
  554. class="control has-addons"
  555. content="Set Playback Rate"
  556. v-tippy
  557. >
  558. <button class="button is-primary">
  559. <i class="material-icons">fast_forward</i>
  560. </button>
  561. <button class="button dropdown-toggle">
  562. <i class="material-icons">
  563. {{
  564. player.showRateDropdown
  565. ? "expand_more"
  566. : "expand_less"
  567. }}
  568. </i>
  569. </button>
  570. </div>
  571. <template #content>
  572. <div class="nav-dropdown-items">
  573. <button
  574. class="nav-item button"
  575. :class="{
  576. active: player.playbackRate === 0.5
  577. }"
  578. title="0.5x"
  579. @click="setPlaybackRate(0.5)"
  580. >
  581. <p>0.5x</p>
  582. </button>
  583. <button
  584. class="nav-item button"
  585. :class="{
  586. active: player.playbackRate === 1
  587. }"
  588. title="1x"
  589. @click="setPlaybackRate(1)"
  590. >
  591. <p>1x</p>
  592. </button>
  593. <button
  594. class="nav-item button"
  595. :class="{
  596. active: player.playbackRate === 2
  597. }"
  598. title="2x"
  599. @click="setPlaybackRate(2)"
  600. >
  601. <p>2x</p>
  602. </button>
  603. </div>
  604. </template>
  605. </tippy>
  606. </div>
  607. <div class="player-footer-center">
  608. <span>
  609. <span>
  610. {{ player.currentTime }}
  611. </span>
  612. /
  613. <span>
  614. {{ player.duration }}
  615. {{ player.videoNote }}
  616. </span>
  617. </span>
  618. </div>
  619. <div class="player-footer-right">
  620. <p id="volume-control">
  621. <i
  622. class="material-icons"
  623. @click="toggleMute()"
  624. :content="`${player.muted ? 'Unmute' : 'Mute'}`"
  625. v-tippy
  626. >{{
  627. player.muted
  628. ? "volume_mute"
  629. : player.volume >= 50
  630. ? "volume_up"
  631. : "volume_down"
  632. }}</i
  633. >
  634. <input
  635. v-model="player.volume"
  636. type="range"
  637. min="0"
  638. max="100"
  639. class="volume-slider active"
  640. @change="changeVolume()"
  641. @input="changeVolume()"
  642. />
  643. </p>
  644. </div>
  645. </div>
  646. </div>
  647. <div v-if="!loaded" class="vertical-padding">
  648. <p>Video hasn't loaded yet</p>
  649. </div>
  650. </template>
  651. <template #footer>
  652. <button
  653. v-if="
  654. hasPermission('songs.create') ||
  655. hasPermission('songs.update')
  656. "
  657. class="button is-primary icon-with-button material-icons"
  658. @click.prevent="
  659. openModal({ modal: 'editSong', data: { song: video } })
  660. "
  661. content="Create/edit song from video"
  662. v-tippy
  663. >
  664. music_note
  665. </button>
  666. <div class="right">
  667. <button
  668. v-if="hasPermission('youtube.removeVideos')"
  669. class="button is-danger icon-with-button material-icons"
  670. @click.prevent="
  671. confirmAction({
  672. message:
  673. 'Removing this video will remove it from all playlists and cause a ratings recalculation.',
  674. action: remove
  675. })
  676. "
  677. content="Delete Video"
  678. v-tippy
  679. >
  680. delete_forever
  681. </button>
  682. </div>
  683. </template>
  684. </modal>
  685. </template>
  686. <style lang="less" scoped>
  687. .night-mode {
  688. .player-section,
  689. .top-section {
  690. background-color: var(--dark-grey-3) !important;
  691. border: 0 !important;
  692. .duration-canvas {
  693. background-color: var(--dark-grey-2) !important;
  694. }
  695. }
  696. }
  697. .top-section {
  698. display: flex;
  699. margin: 0 auto;
  700. padding: 10px;
  701. border: 1px solid var(--light-grey-3);
  702. border-radius: @border-radius;
  703. .left-section {
  704. display: flex;
  705. flex-direction: column;
  706. flex-grow: 1;
  707. p {
  708. text-overflow: ellipsis;
  709. white-space: nowrap;
  710. overflow: hidden;
  711. &:first-child {
  712. margin-top: auto;
  713. }
  714. &:last-child {
  715. margin-bottom: auto;
  716. }
  717. & > span,
  718. & > a {
  719. margin-left: 5px;
  720. }
  721. }
  722. }
  723. :deep(.right-section .thumbnail-preview) {
  724. width: 120px;
  725. height: 120px;
  726. margin: 0;
  727. }
  728. @media (max-width: 600px) {
  729. flex-direction: column-reverse;
  730. .left-section {
  731. margin-top: 10px;
  732. }
  733. }
  734. }
  735. .player-section {
  736. display: flex;
  737. flex-direction: column;
  738. margin: 10px auto 0 auto;
  739. border: 1px solid var(--light-grey-3);
  740. border-radius: @border-radius;
  741. overflow: hidden;
  742. .player-container {
  743. position: relative;
  744. padding-bottom: 56.25%; /* proportion value to aspect ratio 16:9 (9 / 16 = 0.5625 or 56.25%) */
  745. height: 0;
  746. overflow: hidden;
  747. :deep([id^="viewYoutubeVideoPlayer"]) {
  748. position: absolute;
  749. top: 0;
  750. left: 0;
  751. width: 100%;
  752. height: 100%;
  753. min-height: 200px;
  754. }
  755. }
  756. .duration-canvas {
  757. background-color: var(--light-grey-2);
  758. }
  759. .player-error {
  760. display: flex;
  761. height: 428px;
  762. align-items: center;
  763. * {
  764. margin: 0;
  765. flex: 1;
  766. font-size: 30px;
  767. text-align: center;
  768. }
  769. }
  770. .player-footer {
  771. display: flex;
  772. justify-content: space-between;
  773. height: 54px;
  774. padding-left: 10px;
  775. padding-right: 10px;
  776. > * {
  777. width: 33.3%;
  778. display: flex;
  779. align-items: center;
  780. }
  781. .player-footer-left {
  782. flex: 1;
  783. & > .button:not(:first-child) {
  784. margin-left: 5px;
  785. }
  786. :deep(& > .playerRateDropdown) {
  787. margin-left: 5px;
  788. margin-bottom: unset !important;
  789. .control.has-addons {
  790. margin-bottom: unset !important;
  791. & > .button {
  792. font-size: 24px;
  793. }
  794. }
  795. }
  796. :deep(.tippy-box[data-theme~="dropdown"]) {
  797. max-width: 100px !important;
  798. .nav-dropdown-items .nav-item {
  799. justify-content: center !important;
  800. border-radius: @border-radius !important;
  801. &.active {
  802. background-color: var(--primary-color);
  803. color: var(--white);
  804. }
  805. }
  806. }
  807. }
  808. .player-footer-center {
  809. justify-content: center;
  810. align-items: center;
  811. flex: 2;
  812. font-size: 18px;
  813. font-weight: 400;
  814. width: 200px;
  815. margin: 0 5px;
  816. img {
  817. height: 21px;
  818. margin-right: 12px;
  819. filter: invert(26%) sepia(54%) saturate(6317%) hue-rotate(2deg)
  820. brightness(92%) contrast(115%);
  821. }
  822. }
  823. .player-footer-right {
  824. justify-content: right;
  825. flex: 1;
  826. #volume-control {
  827. margin: 3px;
  828. margin-top: 0;
  829. display: flex;
  830. align-items: center;
  831. cursor: pointer;
  832. .volume-slider {
  833. width: 100%;
  834. padding: 0 15px;
  835. background: transparent;
  836. min-width: 100px;
  837. }
  838. input[type="range"] {
  839. -webkit-appearance: none;
  840. margin: 7.3px 0;
  841. }
  842. input[type="range"]:focus {
  843. outline: none;
  844. }
  845. input[type="range"]::-webkit-slider-runnable-track {
  846. width: 100%;
  847. height: 5.2px;
  848. cursor: pointer;
  849. box-shadow: 0;
  850. background: var(--light-grey-3);
  851. border-radius: @border-radius;
  852. border: 0;
  853. }
  854. input[type="range"]::-webkit-slider-thumb {
  855. box-shadow: 0;
  856. border: 0;
  857. height: 19px;
  858. width: 19px;
  859. border-radius: 100%;
  860. background: var(--primary-color);
  861. cursor: pointer;
  862. -webkit-appearance: none;
  863. margin-top: -6.5px;
  864. }
  865. input[type="range"]::-moz-range-track {
  866. width: 100%;
  867. height: 5.2px;
  868. cursor: pointer;
  869. box-shadow: 0;
  870. background: var(--light-grey-3);
  871. border-radius: @border-radius;
  872. border: 0;
  873. }
  874. input[type="range"]::-moz-range-thumb {
  875. box-shadow: 0;
  876. border: 0;
  877. height: 19px;
  878. width: 19px;
  879. border-radius: 100%;
  880. background: var(--primary-color);
  881. cursor: pointer;
  882. -webkit-appearance: none;
  883. margin-top: -6.5px;
  884. }
  885. input[type="range"]::-ms-track {
  886. width: 100%;
  887. height: 5.2px;
  888. cursor: pointer;
  889. box-shadow: 0;
  890. background: var(--light-grey-3);
  891. border-radius: @border-radius;
  892. }
  893. input[type="range"]::-ms-fill-lower {
  894. background: var(--light-grey-3);
  895. border: 0;
  896. border-radius: 0;
  897. box-shadow: 0;
  898. }
  899. input[type="range"]::-ms-fill-upper {
  900. background: var(--light-grey-3);
  901. border: 0;
  902. border-radius: 0;
  903. box-shadow: 0;
  904. }
  905. input[type="range"]::-ms-thumb {
  906. box-shadow: 0;
  907. border: 0;
  908. height: 15px;
  909. width: 15px;
  910. border-radius: 100%;
  911. background: var(--primary-color);
  912. cursor: pointer;
  913. -webkit-appearance: none;
  914. margin-top: 1.5px;
  915. }
  916. }
  917. }
  918. }
  919. }
  920. </style>