From 10ef949e9e4b3f93fb5516d0f7614b99ff36cca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D1=81=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=9F=D0=B5=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0?= Date: Thu, 22 May 2025 09:38:20 +0000 Subject: [PATCH] =?UTF-8?q?#63523=20=D0=92=D1=8B=D0=B1=D0=BE=D1=80=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D0=BC=D0=B5=D1=80=D0=B0=20=D0=BF=D0=BB=D0=B5?= =?UTF-8?q?=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 + src/assets/scss/components/_button.scss | 8 +- src/assets/scss/components/_pip.scss | 13 ++ src/assets/scss/main.scss | 2 + src/components/BottomPanel.ts | 3 +- .../control-bar/PictureInPictureToggle.ts | 143 ++++++++++++++++++ src/constants/VokaSvg.ts | 9 ++ src/internal/player/VokaPlayerCore.ts | 1 + src/languages.json | 6 +- 9 files changed, 186 insertions(+), 6 deletions(-) create mode 100644 src/assets/scss/components/_pip.scss create mode 100644 src/components/control-bar/PictureInPictureToggle.ts diff --git a/README.md b/README.md index e75ec77..fd4672a 100644 --- a/README.md +++ b/README.md @@ -131,4 +131,11 @@ volume: { { selection: true, } +``` + +**enableDocumentPictureInPicture** - включает картинку в картинке, если она доступна для браузера +```js +{ + enableDocumentPictureInPicture: true, +} ``` \ No newline at end of file diff --git a/src/assets/scss/components/_button.scss b/src/assets/scss/components/_button.scss index 4606d84..96626e2 100644 --- a/src/assets/scss/components/_button.scss +++ b/src/assets/scss/components/_button.scss @@ -15,10 +15,12 @@ appearance: none; } -.voka-control .voka-button { - height: 100%; - padding: 0; +.voka-button { cursor: pointer; + + &.voka-disabled { + opacity: 0.5; + } } .voka-control .voka-svg-container:hover svg path { diff --git a/src/assets/scss/components/_pip.scss b/src/assets/scss/components/_pip.scss new file mode 100644 index 0000000..1716698 --- /dev/null +++ b/src/assets/scss/components/_pip.scss @@ -0,0 +1,13 @@ +.vjs-pip-window { + margin: 0; + background: black; + .video-js { + width: 100vw; + height: 100vh; + aspect-ratio: auto; + } + .voka-selection-wrapper, + .voka-bottom-panel { + display: none; + } +} \ No newline at end of file diff --git a/src/assets/scss/main.scss b/src/assets/scss/main.scss index ad74c13..e374778 100644 --- a/src/assets/scss/main.scss +++ b/src/assets/scss/main.scss @@ -38,6 +38,8 @@ @use "../scss/components/audio"; @use "../scss/components/selection"; @use "../scss/components/timeline"; +@use "../scss/components/pip"; + ::-moz-selection {background: rgba(0,0,0,0);} ::selection {background: rgba(0,0,0,0);} diff --git a/src/components/BottomPanel.ts b/src/components/BottomPanel.ts index 5c00918..0a1ab5d 100644 --- a/src/components/BottomPanel.ts +++ b/src/components/BottomPanel.ts @@ -10,6 +10,7 @@ import '@/components/control-bar/ZoomButton' import '@/components/control-bar/subtitles/SubtitlesButton' import '@/components/control-bar/quality-control/QualityButton' import '@/components/control-bar/audio-control/AudioButton' +import '@/components/control-bar/PictureInPictureToggle' /** * Container of main controls. @@ -21,7 +22,7 @@ class BottomPanel extends ClickableComponent { let controls = !!options.playerOptions.controls const _options = { ...options, - children: ['PlayToggle', 'StartOver', 'VolumePanel', 'ProgressControl', 'PlaybackRateButton', 'ZoomButton', 'SubtitlesButton', 'QualityButton', 'AudioButton', 'FullscreenToggle'] + children: ['PlayToggle', 'StartOver', 'VolumePanel', 'ProgressControl', 'PlaybackRateButton', 'ZoomButton', 'SubtitlesButton', 'QualityButton', 'AudioButton', 'PictureInPictureToggle', 'FullscreenToggle'] } super(player, _options) diff --git a/src/components/control-bar/PictureInPictureToggle.ts b/src/components/control-bar/PictureInPictureToggle.ts new file mode 100644 index 0000000..3e79588 --- /dev/null +++ b/src/components/control-bar/PictureInPictureToggle.ts @@ -0,0 +1,143 @@ +import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js' +import document from 'global/document' +import window from 'global/window' + +import { SvgType } from '@/constants/VokaSvg' +import Button from '@/components/Button.ts' +import { TooltipPosition } from '../ClickableComponent' + +const Component = videojs.getComponent('component') +const Browser = videojs.browser + +class PictureInPictureToggle extends Button { + videoEl: any + + constructor(player: VideoJsPlayer, options: VideoJsPlayerOptions) { + const options_ = { + ...options, + svg: SvgType.PICTURE_IN_PICTURE + } + super(player, options_) + + this.on(player, ['enterpictureinpicture', 'leavepictureinpicture'], (e) => + this.handlePiPEnabledChange(e) + ) + this.on(player, ['disablepictureinpicturechanged', 'loadedmetadata'], (e) => + this.handlePiPEnabledChange(e) + ) + this.on( + player, + ['loadedmetadata', 'audioonlymodechange', 'audiopostermodechange'], + () => this.handlePiPAudioModeChange() + ) + this.on(player, ['play'], (e) => this.handlePiPEnabledChange(e)) + + this.controlText('Picture-in-Picture', this.el(), TooltipPosition.topLeft) + + this.disable() + } + + buildCSSClass() { + return `voka-picture-in-picture-control vjs-hidden ${super.buildCSSClass()}` + } + + /** + * Displays or hides the button depending on the audio mode detection. + * Exits picture-in-picture if it is enabled when switching to audio mode. + */ + handlePiPAudioModeChange() { + // This audio detection will not detect HLS or DASH audio-only streams because there was no reliable way to detect them at the time + const isSourceAudio = this.player_.currentType().substring(0, 5) === 'audio' + const isAudioMode = + isSourceAudio || + this.player_.audioPosterMode() || + this.player_.audioOnlyMode() + + if (!isAudioMode) { + this.show() + + return + } + + if (this.player_.isInPictureInPicture()) { + this.player_.exitPictureInPicture() + } + + this.hide() + } + + /** + * Enabled if + * - `player.options().enableDocumentPictureInPicture` is true and + * window.documentPictureInPicture is available; or + * - `player.disablePictureInPicture()` is false and + * element.requestPictureInPicture is available; or + * - (for safari) The videoEl contains webkitSupportsPresentationMode, + * which supports picture-in-picture mode. + */ + checkPiPSupport() { + this.videoEl = this.player_?.tech({ IWillNotUseThisInPlugins: true })?.el() + return ( + (document.pictureInPictureEnabled && + this.player_.disablePictureInPicture() === false) || + (this.player_.options_.playerOptions.enableDocumentPictureInPicture && + 'documentPictureInPicture' in window) || + (this.videoEl && + this.videoEl.webkitSupportsPresentationMode && + this.videoEl.webkitSupportsPresentationMode('picture-in-picture') && + typeof this.videoEl.webkitSetPresentationMode === 'function') + ) + } + + /** + * Enables or disables button based on availability of a Picture-In-Picture mode. + */ + handlePiPEnabledChange() { + if (this.checkPiPSupport()) { + this.enable() + } else { + this.disable() + } + } + + /** + * This gets called when an `PictureInPictureToggle` is "clicked". See + */ + handleClick(event) { + if (Browser.IS_SAFARI) { + this.handleClickForSafari() + return + } + if (!this.player_.isInPictureInPicture()) { + this.player_.requestPictureInPicture() + } else { + this.player_.exitPictureInPicture() + } + } + + handleClickForSafari() { + if (this.checkPiPSupport()) { + this.videoEl.webkitSetPresentationMode( + this.videoEl.webkitPresentationMode === 'picture-in-picture' + ? 'inline' + : 'picture-in-picture' + ) + } + } + + /** + * Show the `Component`s element if it is hidden by removing the + * 'vjs-hidden' class name from it only in browsers that support the Picture-in-Picture API. + */ + show() { + // Does not allow to display the pictureInPictureToggle in browsers that do not support the Picture-in-Picture API, e.g. Firefox. + if (typeof document.exitPictureInPicture !== 'function') { + return + } + + super.show() + } +} + +Component.registerComponent('PictureInPictureToggle', PictureInPictureToggle) +export default PictureInPictureToggle diff --git a/src/constants/VokaSvg.ts b/src/constants/VokaSvg.ts index 8ad20cc..6b0628b 100644 --- a/src/constants/VokaSvg.ts +++ b/src/constants/VokaSvg.ts @@ -17,6 +17,7 @@ export const SvgType = { VOLUME_LOW: 'volume-low', VOLUME_MID: 'volume-mid', VOLUME_HIGH: 'volume-high', + PICTURE_IN_PICTURE: 'picture_in_picture', } export default class VokaSvg { @@ -58,6 +59,8 @@ export default class VokaSvg { return VokaSvg.volumeMid() case SvgType.VOLUME_HIGH: return VokaSvg.volumeHigh() + case SvgType.PICTURE_IN_PICTURE: + return VokaSvg.pictureInPictureButton() default: console.warn('Unknown SvgType received!') @@ -189,6 +192,12 @@ export default class VokaSvg { +` + } + static pictureInPictureButton() { + return ` + + ` } } diff --git a/src/internal/player/VokaPlayerCore.ts b/src/internal/player/VokaPlayerCore.ts index f37f377..d619910 100644 --- a/src/internal/player/VokaPlayerCore.ts +++ b/src/internal/player/VokaPlayerCore.ts @@ -280,6 +280,7 @@ export class VokaPlayerCore { // return `https://streaming.voka.tv/vod_preview/velcom/4W17sRxFu8eCAps3kdTxJrFk7d46DNmum_320x180.jpeg?preview_pos=${percent}`; }, }, + enableDocumentPictureInPicture: true, } } diff --git a/src/languages.json b/src/languages.json index 2ab7fa4..f0fb69d 100644 --- a/src/languages.json +++ b/src/languages.json @@ -24,7 +24,8 @@ "Zoom": "Кадрирование", "Process Selection": "Обработать выделение", "Selection End": "Конец выделения", - "Selection Start": "Начало выделения" + "Selection Start": "Начало выделения", + "Picture-in-Picture": "Картинка в картинке" }, "en": { "Play": "Play", @@ -51,6 +52,7 @@ "Zoom": "Zoom", "Process Selection": "Process Selection", "Selection End": "Selection End", - "Selection Start": "Selection Start" + "Selection Start": "Selection Start", + "Picture-in-Picture": "Picture-in-Picture" } } \ No newline at end of file