11<script setup lang="ts">
22import { ref , computed , watch , onMounted , onBeforeUnmount } from " vue" ;
3- import {
4- Volume2 ,
5- VolumeX ,
6- Maximize2 ,
7- Minimize2 ,
8- PictureInPicture ,
9- } from " lucide-vue-next" ;
3+ import { Volume2 , VolumeX , Maximize2 , Minimize2 } from " lucide-vue-next" ;
104import { useClipRenderActive } from " ~/composables/useClipRenderActive" ;
115
126const { active : clipRenderActive } = useClipRenderActive ();
@@ -137,66 +131,6 @@ function onWebkitEndFullscreen() {
137131}
138132
139133// Picture-in-Picture — only surfaced on mobile for live game streams.
140- // Demo/highlights set `enablePip=false` and keep `disablepictureinpicture`
141- // on the <video>, which is what Chrome reads when deciding whether to
142- // honor a PIP request.
143- const isPip = ref (false );
144- const showPip = computed (() => {
145- if (! props .enablePip ) return false ;
146- if (! coarsePointer .value ) return false ;
147- if (typeof document === " undefined" ) return false ;
148- const doc = document as Document & { pictureInPictureEnabled? : boolean };
149- const video = videoRef .value as IosVideo | null ;
150- return !! (
151- doc .pictureInPictureEnabled ||
152- video ?.webkitSupportsPresentationMode ?.(" picture-in-picture" )
153- );
154- });
155-
156- async function togglePip() {
157- const video = videoRef .value as IosVideo | null ;
158- if (! video ) return ;
159- const doc = document as Document & {
160- pictureInPictureElement? : Element | null ;
161- exitPictureInPicture? : () => Promise <void >;
162- };
163- try {
164- // iOS Safari path: standard requestPictureInPicture is shipped on
165- // iPadOS 14+ but iPhone Safari still routes everything through the
166- // webkit presentation-mode API.
167- if (
168- video .webkitSupportsPresentationMode ?.(" picture-in-picture" ) &&
169- typeof video .webkitSetPresentationMode === " function" &&
170- ! doc .pictureInPictureEnabled
171- ) {
172- const next =
173- video .webkitPresentationMode === " picture-in-picture"
174- ? " inline"
175- : " picture-in-picture" ;
176- video .webkitSetPresentationMode (next );
177- return ;
178- }
179- if (doc .pictureInPictureElement ) {
180- await doc .exitPictureInPicture ?.();
181- } else {
182- await video .requestPictureInPicture ();
183- }
184- } catch (err ) {
185- console .debug (" [whep] pip toggle failed:" , err );
186- }
187- }
188-
189- function onEnterPip() {
190- isPip .value = true ;
191- }
192- function onLeavePip() {
193- isPip .value = false ;
194- }
195- function onWebkitPresentationModeChanged() {
196- const video = videoRef .value as IosVideo | null ;
197- isPip .value = video ?.webkitPresentationMode === " picture-in-picture" ;
198- }
199-
200134function onKeyDown(e : KeyboardEvent ) {
201135 if (e .metaKey || e .ctrlKey || e .altKey ) return ;
202136 const target = e .target ;
@@ -504,12 +438,6 @@ onMounted(() => {
504438 // instead of the document fullscreenchange path.
505439 video .addEventListener (" webkitbeginfullscreen" , onWebkitBeginFullscreen );
506440 video .addEventListener (" webkitendfullscreen" , onWebkitEndFullscreen );
507- video .addEventListener (" enterpictureinpicture" , onEnterPip );
508- video .addEventListener (" leavepictureinpicture" , onLeavePip );
509- video .addEventListener (
510- " webkitpresentationmodechanged" ,
511- onWebkitPresentationModeChanged ,
512- );
513441 video .addEventListener (" pause" , onInvoluntaryPause );
514442 }
515443});
@@ -550,12 +478,6 @@ onBeforeUnmount(() => {
550478 if (video ) {
551479 video .removeEventListener (" webkitbeginfullscreen" , onWebkitBeginFullscreen );
552480 video .removeEventListener (" webkitendfullscreen" , onWebkitEndFullscreen );
553- video .removeEventListener (" enterpictureinpicture" , onEnterPip );
554- video .removeEventListener (" leavepictureinpicture" , onLeavePip );
555- video .removeEventListener (
556- " webkitpresentationmodechanged" ,
557- onWebkitPresentationModeChanged ,
558- );
559481 video .removeEventListener (" pause" , onInvoluntaryPause );
560482 }
561483});
@@ -646,10 +568,7 @@ defineExpose({ connect, teardown });
646568 </span >
647569 </button >
648570
649- <!-- Audio + fullscreen tray (visible only when audio is on).
650- On coarse pointers (touch) drop the hover-gated opacity so the
651- tray stays reachable — hover is a flaky proxy for "is the
652- viewer engaged" on phones and tablets. -->
571+ <!-- Audio tray (right) — visible only when audio is on. -->
653572 <div
654573 v-if =" status === 'playing' && !useFallback && !isMuted"
655574 class =" absolute bottom-2 right-2 z-10 flex items-center gap-2 transition-opacity duration-150"
@@ -679,43 +598,18 @@ defineExpose({ connect, teardown });
679598 @input =" setVolume(Number(($event.target as HTMLInputElement).value))"
680599 />
681600 </div >
682- <button
683- v-if =" showPip"
684- type =" button"
685- :aria-label ="
686- isPip
687- ? $t('ui_extras.exit_picture_in_picture')
688- : $t('ui_extras.picture_in_picture')
689- "
690- :title =" $t('replay_extras.picture_in_picture')"
691- class =" inline-flex size-7 items-center justify-center rounded-full border border-white/20 bg-black/60 text-white/90 backdrop-blur-sm transition-all duration-150 hover:bg-black/80 hover:text-white hover:scale-110 cursor-pointer"
692- @click =" togglePip"
693- >
694- <PictureInPicture class="size-3.5 " />
695- </button >
696- <button
697- type =" button"
698- :aria-label ="
699- isFullscreen
700- ? $t('ui_extras.exit_fullscreen')
701- : $t('ui_extras.fullscreen')
702- "
703- :title =" $t('replay_extras.fullscreen_key')"
704- class =" inline-flex size-7 items-center justify-center rounded-full border border-white/20 bg-black/60 text-white/90 backdrop-blur-sm transition-all duration-150 hover:bg-black/80 hover:text-white hover:scale-110 cursor-pointer"
705- @click =" toggleFullscreen"
706- >
707- <Minimize2 v-if =" isFullscreen " class="size-3.5 " />
708- <Maximize2 v-else class="size-3.5 " />
709- </button >
710601 </div >
711602
712- <!-- Always-available fullscreen + PIP entry, bottom-left while the
713- unmute pill is occupying the bottom-right corner. Touch devices
714- keep these buttons visible (no hover gating). -->
603+ <!-- Fullscreen — single left-side button. Hover-gated on desktop;
604+ always visible on touch devices. -->
715605 <div
716- v-if =" status === 'playing' && !useFallback && isMuted "
606+ v-if =" status === 'playing' && !useFallback"
717607 class =" absolute bottom-3 left-3 z-10 flex items-center gap-2 transition-opacity duration-150"
718- :class =" coarsePointer ? '' : 'opacity-0 group-hover:opacity-100'"
608+ :class ="
609+ coarsePointer
610+ ? ''
611+ : 'opacity-0 group-hover:opacity-100 focus-within:opacity-100'
612+ "
719613 >
720614 <button
721615 type =" button"
@@ -731,20 +625,6 @@ defineExpose({ connect, teardown });
731625 <Minimize2 v-if =" isFullscreen " class="size-3.5 " />
732626 <Maximize2 v-else class="size-3.5 " />
733627 </button >
734- <button
735- v-if =" showPip"
736- type =" button"
737- :aria-label ="
738- isPip
739- ? $t('ui_extras.exit_picture_in_picture')
740- : $t('ui_extras.picture_in_picture')
741- "
742- :title =" $t('replay_extras.picture_in_picture')"
743- class =" inline-flex size-7 items-center justify-center rounded-full border border-white/20 bg-black/60 text-white/90 backdrop-blur-sm transition-all duration-150 hover:bg-black/80 hover:text-white hover:scale-110 cursor-pointer"
744- @click =" togglePip"
745- >
746- <PictureInPicture class="size-3.5 " />
747- </button >
748628 </div >
749629 <!-- "Rendering clip" chip. Shown over the last-good frame while
750630 the pod has stopped its publisher to free the GPU for a
0 commit comments