ViewYoutubeVideo.vue 23 KB

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