@leavepulse/ui 0.82.0 → 0.83.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/LpVideoTrim-BcGs3Sh0.css +8 -0
- package/dist/assets/VideoFlash-VuPkC7QV.css +23 -0
- package/dist/chunks/{ContextMenuItems-kRVTuSZd.js → ContextMenuItems-h-apPY2d.js} +1 -1
- package/dist/chunks/{DropzoneLinkField-CvMUu8HQ.js → DropzoneLinkField-_Op8b_ee.js} +1 -1
- package/dist/chunks/{FieldLeading-CNN_5Dp1.js → FieldLeading-mOMcwNqi.js} +1 -1
- package/dist/chunks/{FieldWrapper-BBJPEVHr.js → FieldWrapper-DtT-AIcn.js} +1 -1
- package/dist/chunks/{IconCloseButton-C59_etp2.js → IconCloseButton-YmisskyZ.js} +1 -1
- package/dist/chunks/{LayoutCanvas-BIY74SUe.js → LayoutCanvas-BrISZEBr.js} +1 -1
- package/dist/chunks/{LayoutNode-CTlKlhb-.js → LayoutNode-CwMQ8ACa.js} +1 -1
- package/dist/chunks/{LpActionBar-Cowb-28D.js → LpActionBar-CVVC6YbC.js} +1 -1
- package/dist/chunks/{LpAddressInput-nTJlUQ6K.js → LpAddressInput-tiuJ-bru.js} +7 -7
- package/dist/chunks/{LpAlert-DhZdjekN.js → LpAlert-8-x4H_Nk.js} +1 -1
- package/dist/chunks/{LpAppShell-CK9hTgQ1.js → LpAppShell-BW2YMq-A.js} +4 -4
- package/dist/chunks/{LpAutocomplete--PaZkQoU.js → LpAutocomplete-BZODIops.js} +4 -4
- package/dist/chunks/{LpAvatar-DVCld_L2.js → LpAvatar-BZq4nH53.js} +1 -1
- package/dist/chunks/{LpBadge-DXvpxk6z.js → LpBadge-Cz0pSSUu.js} +1 -1
- package/dist/chunks/{LpBottomNav-COQiBNUN.js → LpBottomNav-DVdR5Mry.js} +1 -1
- package/dist/chunks/{LpBreadcrumbs-Dt1O5G4O.js → LpBreadcrumbs-B2jphAel.js} +1 -1
- package/dist/chunks/{LpCalendar-C8Xlukd2.js → LpCalendar-C0_N5LVT.js} +1 -1
- package/dist/chunks/{LpCheckbox-Dpybd3oV.js → LpCheckbox-CwqLNdw4.js} +2 -2
- package/dist/chunks/{LpCodeBlock-C1Hu5RyT.js → LpCodeBlock-Dcss6bnl.js} +2 -2
- package/dist/chunks/{LpCodeDisplay-BVruT3-A.js → LpCodeDisplay-CLms1zDY.js} +4 -4
- package/dist/chunks/{LpColorPicker-BK-NRaGK.js → LpColorPicker-CbIY450X.js} +3 -3
- package/dist/chunks/{LpConfirmDialog-2CQFKtqU.js → LpConfirmDialog-RmP4z6va.js} +2 -2
- package/dist/chunks/{LpConfirmHost-CTkSytVl.js → LpConfirmHost-DheBgRMB.js} +1 -1
- package/dist/chunks/{LpContextMenu-DcHPdW-t.js → LpContextMenu-DIYhlap8.js} +2 -2
- package/dist/chunks/{LpContextSwitcher-C_nnDbeI.js → LpContextSwitcher-fDA6ERl-.js} +2 -2
- package/dist/chunks/{LpCopyButton-De7_elaO.js → LpCopyButton-DSqKFGEb.js} +1 -1
- package/dist/chunks/{LpDataList-BjZPfQjO.js → LpDataList-f9UjSx9m.js} +2 -2
- package/dist/chunks/{LpDatePicker-wOrHd0fg.js → LpDatePicker-3kDQLVbg.js} +5 -5
- package/dist/chunks/{LpDateRangePicker-CVMvvo5t.js → LpDateRangePicker-DUPiTt0_.js} +3 -3
- package/dist/chunks/{LpDisclosure-CtQ63LeO.js → LpDisclosure-C5WZjhbQ.js} +1 -1
- package/dist/chunks/{LpDrawer-TZBPzyOM.js → LpDrawer-CkYvIGIa.js} +1 -1
- package/dist/chunks/{LpDropdownMenu-DL9LgkS8.js → LpDropdownMenu-zSsK2Xra.js} +6 -3
- package/dist/chunks/{LpEditable-DuPE2tkl.js → LpEditable-CP7Y_lKg.js} +2 -2
- package/dist/chunks/{LpEmptyState-W1U_tKg-.js → LpEmptyState-dsc5x-ya.js} +1 -1
- package/dist/chunks/{LpFieldNotice-BLKlYLwu.js → LpFieldNotice-TOTbWvgv.js} +1 -1
- package/dist/chunks/{LpFileTree-CEiRIpj1.js → LpFileTree-BPjnOTln.js} +4 -4
- package/dist/chunks/{LpFileTreeNode-C0292AU2.js → LpFileTreeNode-_-MiWmie.js} +4 -4
- package/dist/chunks/{LpFormField-Bhgi0idH.js → LpFormField-BJwD1iEF.js} +1 -1
- package/dist/chunks/{LpHintLayer-D8ZKsvsT.js → LpHintLayer-CmfGnxYu.js} +1 -1
- package/dist/chunks/{LpHints-DbIV-wk4.js → LpHints-C2Xvc1Fd.js} +3 -3
- package/dist/chunks/{LpIcon-CytpcLmS.js → LpIcon-CoOLz1oJ.js} +1 -1
- package/dist/chunks/{LpInfraNode-8cGoY87Q.js → LpInfraNode-CW3qsM-U.js} +2 -2
- package/dist/chunks/{LpInput-P2C12X7s.js → LpInput-BVWhkPuN.js} +3 -3
- package/dist/chunks/{LpInspector-DjfMa4tD.js → LpInspector-REmoX_es.js} +4 -4
- package/dist/chunks/{LpLightbox-BEQtCz0m.js → LpLightbox-C3FFTYP5.js} +65 -20
- package/dist/chunks/{LpLogInspector-CyxK1d-V.js → LpLogInspector-Cs8Z_fM0.js} +2 -2
- package/dist/chunks/{LpLogViewer-CZrc_HG7.js → LpLogViewer-DQRGe-ZK.js} +6 -6
- package/dist/chunks/{LpMapPicker-CbTfFcEu.js → LpMapPicker-CQnQPgKm.js} +1 -1
- package/dist/chunks/{LpMediaGallery-C_SwNR4y.js → LpMediaGallery-DkKqTy9P.js} +20 -8
- package/dist/chunks/{LpModal-B6Yt0G7E.js → LpModal-DwejR7VP.js} +1 -1
- package/dist/chunks/{LpModalNav-CMYWEiUs.js → LpModalNav-D1-dSe6f.js} +3 -3
- package/dist/chunks/{LpNotificationBell-BJKPx0X1.js → LpNotificationBell-EKLkN0sp.js} +4 -4
- package/dist/chunks/{LpNotificationFeed-orWISVez.js → LpNotificationFeed-Dc-yQ4NH.js} +4 -4
- package/dist/chunks/{LpNumberField-C5MeLsoE.js → LpNumberField-DvUjEMp-.js} +3 -3
- package/dist/chunks/{LpOtpInput-6SCQ64uK.js → LpOtpInput-xvDO14f4.js} +1 -1
- package/dist/chunks/{LpPagination-CkwbdFa9.js → LpPagination-BFZY8dFn.js} +1 -1
- package/dist/chunks/{LpPaletteTheme-RWgbWFOz.js → LpPaletteTheme-EBJv0Bwz.js} +2 -2
- package/dist/chunks/{LpPasswordInput-DJBRo5lt.js → LpPasswordInput-DRJR3fF1.js} +2 -2
- package/dist/chunks/{LpPhoneInput-DcmcJwos.js → LpPhoneInput-BGOOtUoW.js} +3 -3
- package/dist/chunks/{LpPie-DmMlhgo2.js → LpPie-Deyn8h5J.js} +2 -2
- package/dist/chunks/{LpPipeline-BXcp2shV.js → LpPipeline-9ChjeNh5.js} +2 -2
- package/dist/chunks/{LpQuota-CtEiqRg4.js → LpQuota-CNh5eF_p.js} +1 -1
- package/dist/chunks/{LpRadioGroup-Ubkk_eSq.js → LpRadioGroup-Br2Nl9Gr.js} +2 -2
- package/dist/chunks/{LpRangeCalendar-DCswVYbU.js → LpRangeCalendar-D66Y3U_w.js} +1 -1
- package/dist/chunks/{LpRangeSlider-DM1qy5Tp.js → LpRangeSlider-eX-fIlOw.js} +1 -1
- package/dist/chunks/{LpRepeater-BMo2Eabn.js → LpRepeater-0XvnlVd_.js} +2 -2
- package/dist/chunks/{LpSecretField-SSRGVqRc.js → LpSecretField-1RCooChq.js} +5 -5
- package/dist/chunks/{LpSection-C3bTxD7t.js → LpSection-B9gbJJcP.js} +1 -1
- package/dist/chunks/{LpSegmented-C1DF6m6A.js → LpSegmented-hIjGzGTo.js} +2 -2
- package/dist/chunks/{LpSelect-DvDr1VPn.js → LpSelect-DugubwTc.js} +4 -4
- package/dist/chunks/{LpSidebar-Bql881r6.js → LpSidebar-DevSoN8W.js} +4 -4
- package/dist/chunks/{LpSidebarNav-CKx2KJvr.js → LpSidebarNav-DwQQ3KXe.js} +3 -3
- package/dist/chunks/{LpSidebarRow-BMOL9kOG.js → LpSidebarRow-CAGgtJCS.js} +3 -3
- package/dist/chunks/{LpSlider-DsSMe4b5.js → LpSlider-1fHpVxpB.js} +1 -1
- package/dist/chunks/{LpStat-CQYYDMS6.js → LpStat-Cm9-FaWQ.js} +2 -2
- package/dist/chunks/{LpStatus-g5nSJfU3.js → LpStatus-CE8FSAT1.js} +1 -1
- package/dist/chunks/{LpStepMark-seVNOuNJ.js → LpStepMark-BWSlV69R.js} +1 -1
- package/dist/chunks/{LpStepper-D6qEXihZ.js → LpStepper-1D0ZA7Nx.js} +1 -1
- package/dist/chunks/{LpSwitch-DM5CVm31.js → LpSwitch-D_kvE0oQ.js} +1 -1
- package/dist/chunks/{LpTabFace-ZWwelqRU.js → LpTabFace-CpbFKi-N.js} +2 -2
- package/dist/chunks/{LpTableFilter-CWU9Dmk-.js → LpTableFilter-BYHyCu38.js} +2 -2
- package/dist/chunks/{LpTagsInput-C2qVF2ax.js → LpTagsInput-Cqm38t5N.js} +5 -5
- package/dist/chunks/{LpTextarea-C3EoV_xv.js → LpTextarea-DaSKBkiV.js} +1 -1
- package/dist/chunks/{LpThemeStudio-BIBahaIs.js → LpThemeStudio-BNdfi-Ds.js} +5 -5
- package/dist/chunks/{LpThemeSwitcher-D4MqFLGS.js → LpThemeSwitcher-CbC1nqBP.js} +3 -3
- package/dist/chunks/{LpTimeField-DPOkbvyP.js → LpTimeField-CCfPCWPT.js} +2 -2
- package/dist/chunks/{LpToaster-Dj1VKxFv.js → LpToaster-BhlI7h-h.js} +2 -2
- package/dist/chunks/{LpTopologyCanvas-Bspzwm7x.js → LpTopologyCanvas-DlKbu7E5.js} +4 -4
- package/dist/chunks/{LpTour-CwpVdVN-.js → LpTour-CSLerA1B.js} +1 -1
- package/dist/chunks/{LpTruncate-JSaIW25q.js → LpTruncate-Ct26BSYB.js} +1 -1
- package/dist/chunks/{LpVideoBackdrop-Ct4St4ZL.js → LpVideoBackdrop-57i8RY8g.js} +1 -2
- package/dist/chunks/LpVideoPlayer-DcWd9DDW.js +813 -0
- package/dist/chunks/{LpWindow-RVcP436Z.js → LpWindow-YnF_knSj.js} +2 -2
- package/dist/chunks/{LpWorkspaceSwitcher-D0WOz1Fg.js → LpWorkspaceSwitcher-B-5UU6o4.js} +3 -3
- package/dist/chunks/{MenuTitle-D8Umab23.js → MenuTitle-BnZbFSwq.js} +1 -1
- package/dist/chunks/{OptionContent-DCnX_KRK.js → OptionContent-D5e_eklA.js} +1 -1
- package/dist/chunks/{PickerField-ChjY27bm.js → PickerField-KpHiowNI.js} +1 -1
- package/dist/chunks/{RevealToggle-MaL7PEjr.js → RevealToggle-CE1hAIJB.js} +1 -1
- package/dist/chunks/SteadyText-CwH-kduj.js +25 -0
- package/dist/chunks/{TourCard-oXZSpHKb.js → TourCard-t2csiTDS.js} +1 -1
- package/dist/chunks/VideoThumb-Csf9bUz3.js +67 -0
- package/dist/chunks/VideoVolume-B4_DNmNi.js +68 -0
- package/dist/chunks/icons-CdSlCrle.js +59 -0
- package/dist/component-names.d.ts +1 -1
- package/dist/components/ContextMenuItems.vue.js +1 -1
- package/dist/components/DropzoneLinkField.vue.js +1 -1
- package/dist/components/FieldLeading.vue.js +1 -1
- package/dist/components/FieldWrapper.vue.js +1 -1
- package/dist/components/IconCloseButton.vue.js +1 -1
- package/dist/components/LayoutCanvas.vue.js +1 -1
- package/dist/components/LayoutNode.vue.js +1 -1
- package/dist/components/LpActionBar.vue.js +1 -1
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAlert.vue.js +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpAutocomplete.vue.js +1 -1
- package/dist/components/LpAvatar.vue.js +1 -1
- package/dist/components/LpBadge.vue.js +1 -1
- package/dist/components/LpBottomNav.vue.js +1 -1
- package/dist/components/LpBreadcrumbs.vue.js +1 -1
- package/dist/components/LpCalendar.vue.js +1 -1
- package/dist/components/LpChart.vue.js +2 -2
- package/dist/components/LpCheckbox.vue.js +1 -1
- package/dist/components/LpCodeBlock.vue.js +1 -1
- package/dist/components/LpCodeDisplay.vue.js +1 -1
- package/dist/components/LpColorPicker.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.js +2 -2
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpConfirmHost.vue.js +1 -1
- package/dist/components/LpContextMenu.vue.js +1 -1
- package/dist/components/LpContextSwitcher.vue.js +1 -1
- package/dist/components/LpCopyButton.vue.js +1 -1
- package/dist/components/LpDataList.vue.js +1 -1
- package/dist/components/LpDatePicker.vue.js +1 -1
- package/dist/components/LpDateRangePicker.vue.js +1 -1
- package/dist/components/LpDisclosure.vue.js +1 -1
- package/dist/components/LpDock.vue.js +1 -1
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpDropdownMenu.vue.d.ts +5 -1
- package/dist/components/LpDropdownMenu.vue.js +1 -1
- package/dist/components/LpDropzone.vue.js +2 -2
- package/dist/components/LpEditable.vue.js +1 -1
- package/dist/components/LpEmptyState.vue.js +1 -1
- package/dist/components/LpFieldNotice.vue.js +1 -1
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpFormField.vue.js +1 -1
- package/dist/components/LpHintLayer.vue.js +1 -1
- package/dist/components/LpHints.vue.js +1 -1
- package/dist/components/LpIcon.vue.js +1 -1
- package/dist/components/LpImageEditor.vue.js +1 -1
- package/dist/components/LpInfraNode.vue.js +1 -1
- package/dist/components/LpInput.vue.js +1 -1
- package/dist/components/LpInspector.vue.js +1 -1
- package/dist/components/LpLightbox.vue.js +1 -1
- package/dist/components/LpLogInspector.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpMapPicker.vue.js +1 -1
- package/dist/components/LpMediaGallery.vue.d.ts +1 -1
- package/dist/components/LpMediaGallery.vue.js +1 -1
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpNotificationFeed.vue.js +1 -1
- package/dist/components/LpNumberField.vue.js +1 -1
- package/dist/components/LpOtpInput.vue.js +1 -1
- package/dist/components/LpPagination.vue.js +1 -1
- package/dist/components/LpPaletteFacts.vue.js +1 -1
- package/dist/components/LpPaletteHeader.vue.js +2 -2
- package/dist/components/LpPaletteTheme.vue.js +1 -1
- package/dist/components/LpPasswordInput.vue.js +1 -1
- package/dist/components/LpPhoneInput.vue.js +1 -1
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpPipeline.vue.js +1 -1
- package/dist/components/LpQuota.vue.js +1 -1
- package/dist/components/LpRadioGroup.vue.js +1 -1
- package/dist/components/LpRangeCalendar.vue.js +1 -1
- package/dist/components/LpRangeSlider.vue.js +1 -1
- package/dist/components/LpRepeater.vue.js +1 -1
- package/dist/components/LpSecretField.vue.js +1 -1
- package/dist/components/LpSection.vue.js +1 -1
- package/dist/components/LpSegmented.vue.js +1 -1
- package/dist/components/LpSelect.vue.js +1 -1
- package/dist/components/LpShortcutsDialog.vue.js +1 -1
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSidebarNav.vue.js +1 -1
- package/dist/components/LpSidebarRow.vue.js +1 -1
- package/dist/components/LpSlider.vue.js +1 -1
- package/dist/components/LpStat.vue.js +1 -1
- package/dist/components/LpStatus.vue.js +1 -1
- package/dist/components/LpStepMark.vue.js +1 -1
- package/dist/components/LpStepper.vue.js +1 -1
- package/dist/components/LpSwitch.vue.js +1 -1
- package/dist/components/LpTabFace.vue.js +1 -1
- package/dist/components/LpTable.vue.js +9 -9
- package/dist/components/LpTableFilter.vue.js +1 -1
- package/dist/components/LpTabs.vue.js +2 -2
- package/dist/components/LpTagsInput.vue.js +1 -1
- package/dist/components/LpTextarea.vue.js +1 -1
- package/dist/components/LpThemeStudio.vue.js +1 -1
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/LpTimeField.vue.js +1 -1
- package/dist/components/LpToaster.vue.js +1 -1
- package/dist/components/LpTopologyCanvas.vue.js +1 -1
- package/dist/components/LpTour.vue.js +1 -1
- package/dist/components/LpTruncate.vue.js +1 -1
- package/dist/components/LpVideoBackdrop.vue.js +1 -1
- package/dist/components/LpVideoPlayer.vue.d.ts +111 -0
- package/dist/components/LpVideoPlayer.vue.js +4 -0
- package/dist/components/LpVideoTrim.vue.d.ts +19 -5
- package/dist/components/LpVideoTrim.vue.js +596 -445
- package/dist/components/LpWindow.vue.js +1 -1
- package/dist/components/LpWorkspaceSwitcher.vue.js +1 -1
- package/dist/components/MenuTitle.vue.js +1 -1
- package/dist/components/OptionContent.vue.js +1 -1
- package/dist/components/PickerField.vue.js +1 -1
- package/dist/components/RevealToggle.vue.js +1 -1
- package/dist/components/SteadyText.vue.d.ts +7 -0
- package/dist/components/SteadyText.vue.js +4 -0
- package/dist/components/TourCard.vue.js +1 -1
- package/dist/components/VideoFlash.vue.d.ts +10 -0
- package/dist/components/VideoFlash.vue.js +33 -0
- package/dist/components/VideoThumb.vue.d.ts +12 -0
- package/dist/components/VideoThumb.vue.js +4 -0
- package/dist/components/VideoVolume.vue.d.ts +15 -0
- package/dist/components/VideoVolume.vue.js +4 -0
- package/dist/components/lightbox.d.ts +13 -0
- package/dist/components/lightbox.js +11 -1
- package/dist/components/menuWhen.js +1 -1
- package/dist/components/videoBackdrop.d.ts +15 -2
- package/dist/components/videoBackdrop.js +8 -2
- package/dist/components/videoMotion.d.ts +28 -0
- package/dist/components/videoMotion.js +26 -0
- package/dist/components/videoTrim.d.ts +2 -9
- package/dist/components/videoTrim.js +0 -6
- package/dist/composables/useVideoMedia.d.ts +74 -0
- package/dist/composables/useVideoMedia.js +291 -0
- package/dist/icons-all.js +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +173 -168
- package/package.json +5 -2
- package/src/component-names.ts +1 -0
- package/src/components/LpDropdownMenu.vue +4 -0
- package/src/components/LpLightbox.vue +61 -8
- package/src/components/LpMediaGallery.vue +19 -2
- package/src/components/LpVideoBackdrop.vue +1 -7
- package/src/components/LpVideoPlayer.vue +774 -0
- package/src/components/LpVideoTrim.vue +437 -295
- package/src/components/SteadyText.vue +21 -0
- package/src/components/VideoFlash.vue +52 -0
- package/src/components/VideoThumb.vue +66 -0
- package/src/components/VideoVolume.vue +63 -0
- package/src/components/lightbox.ts +23 -0
- package/src/components/videoBackdrop.ts +20 -3
- package/src/components/videoMotion.ts +38 -0
- package/src/components/videoTrim.ts +2 -14
- package/src/composables/useVideoMedia.ts +411 -0
- package/src/index.ts +4 -1
- package/src/kit-icons.ts +1 -1
- package/dist/assets/LpVideoTrim-Ci6IIHW0.css +0 -8
- package/dist/chunks/icons-BOdqDUii.js +0 -59
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { onBeforeUnmount, ref, watch, type Ref } from "vue"
|
|
2
|
+
import { SLOWEST_RATE } from "../components/videoBackdrop"
|
|
3
|
+
import { nodeWindow } from "./realm"
|
|
4
|
+
import { usePersisted } from "./usePersisted"
|
|
5
|
+
import { usePrefersReducedMotion } from "./useReducedMotion"
|
|
6
|
+
import { useTween } from "./useTween"
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* The state of a <video> as a component that draws its own controls needs it,
|
|
10
|
+
* shared by LpVideoPlayer and LpVideoTrim so both know the same things about
|
|
11
|
+
* a video the same way:
|
|
12
|
+
*
|
|
13
|
+
* - One seek at a time, the latest wins: a drag asks for a position on every
|
|
14
|
+
* pointer move, and WebKitGTK's pipeline stalls on a seek started inside
|
|
15
|
+
* another.
|
|
16
|
+
* - A MediaRecorder WebM carries no duration until its end has been seen: it
|
|
17
|
+
* is found by seeking far past it once.
|
|
18
|
+
* - Chromium answers a 404, or a login page served in place of an expired
|
|
19
|
+
* session's video, with "format not supported", the same code as a real
|
|
20
|
+
* codec problem; one byte of the address tells the two apart.
|
|
21
|
+
* - The playhead follows the picture every frame while playing, not the four
|
|
22
|
+
* `timeupdate`s a second.
|
|
23
|
+
*
|
|
24
|
+
* Bind `listeners` on the element with `v-on`.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Why there is no video to show:
|
|
29
|
+
* - "load": the address did not answer (network, 404, an expired session);
|
|
30
|
+
* - "decode": the file arrived but is damaged;
|
|
31
|
+
* - "unsupported": a format or codec this browser cannot play (H.265 in
|
|
32
|
+
* Chromium without hardware support, ProRes anywhere).
|
|
33
|
+
*/
|
|
34
|
+
export type VideoErrorCause = "load" | "decode" | "unsupported"
|
|
35
|
+
|
|
36
|
+
export function mediaErrorCause(error: MediaError | null): VideoErrorCause {
|
|
37
|
+
if (error?.code === 3) return "decode"
|
|
38
|
+
if (error?.code === 4) return "unsupported"
|
|
39
|
+
return "load"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface UseVideoMediaOptions {
|
|
43
|
+
src: () => string
|
|
44
|
+
/** Every frame while playing, after `time` is updated. */
|
|
45
|
+
onFrame?: () => void
|
|
46
|
+
onError?: (cause: VideoErrorCause) => void
|
|
47
|
+
/** Ease starts and stops (true by default); false cuts in and out at once. */
|
|
48
|
+
ease?: () => boolean
|
|
49
|
+
/**
|
|
50
|
+
* Keep volume, mute and speed between videos on this device, shared by every
|
|
51
|
+
* player and editor of the kit: someone who turned the sound down turned it
|
|
52
|
+
* down, whichever component the next video is in. False forgets.
|
|
53
|
+
*/
|
|
54
|
+
remember?: () => boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface Sound {
|
|
58
|
+
volume: number
|
|
59
|
+
muted: boolean
|
|
60
|
+
rate: number
|
|
61
|
+
}
|
|
62
|
+
/*
|
|
63
|
+
* A kept record is used only when it is one a person could have chosen: a
|
|
64
|
+
* speed outside what the menus offer, or a volume outside 0..1, is a record
|
|
65
|
+
* written by an easing caught mid-way (a sixteenth speed and silence, once),
|
|
66
|
+
* and restoring it would start every video stuck.
|
|
67
|
+
*/
|
|
68
|
+
const isSound = (v: unknown): v is Sound => {
|
|
69
|
+
if (!v || typeof v !== "object") return false
|
|
70
|
+
const { volume, muted, rate } = v as Sound
|
|
71
|
+
return typeof muted === "boolean" && typeof volume === "number" && volume >= 0 && volume <= 1 && typeof rate === "number" && rate >= 0.25 && rate <= 4
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function useVideoMedia(el: Ref<HTMLVideoElement | null>, options: UseVideoMediaOptions) {
|
|
75
|
+
const duration = ref(0)
|
|
76
|
+
const time = ref(0)
|
|
77
|
+
const playing = ref(false)
|
|
78
|
+
/** Playback stalled for data. */
|
|
79
|
+
const waiting = ref(false)
|
|
80
|
+
const ended = ref(false)
|
|
81
|
+
const muted = ref(false)
|
|
82
|
+
const volume = ref(1)
|
|
83
|
+
const rate = ref(1)
|
|
84
|
+
/** Buffered stretches as fractions of the duration. */
|
|
85
|
+
const buffered = ref<[number, number][]>([])
|
|
86
|
+
const failure = ref<VideoErrorCause | null>(null)
|
|
87
|
+
const size = ref({ width: 0, height: 0 })
|
|
88
|
+
|
|
89
|
+
const sound = usePersisted<Sound>(() => (options.remember?.() ?? true) && "lp-video:sound", { volume: 1, muted: false, rate: 1 }, { validate: isSound })
|
|
90
|
+
/* Applied once per address, when it first knows its duration; saved only after, so a fresh element's defaults never overwrite what was kept. */
|
|
91
|
+
let applied = ""
|
|
92
|
+
|
|
93
|
+
let seekBusy = false
|
|
94
|
+
let seekTarget: number | null = null
|
|
95
|
+
let probingDuration = false
|
|
96
|
+
let frame = 0
|
|
97
|
+
|
|
98
|
+
function seek(t: number) {
|
|
99
|
+
const video = el.value
|
|
100
|
+
const target = Math.min(Math.max(0, t), duration.value || t)
|
|
101
|
+
time.value = target
|
|
102
|
+
ended.value = false
|
|
103
|
+
if (!video) return
|
|
104
|
+
if (seekBusy) {
|
|
105
|
+
seekTarget = target
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
seekBusy = true
|
|
109
|
+
video.currentTime = target
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/*
|
|
113
|
+
* `currentTime` moves in steps, one per decoded frame: at a few frames a
|
|
114
|
+
* second (a screen recording, a slideshow) a bar or a playhead driven by it
|
|
115
|
+
* jumps along. Between steps the position runs on the clock from the last
|
|
116
|
+
* one, at the playback rate; it never goes back (a step that lands a hair
|
|
117
|
+
* behind the estimate is held), and never runs more than a moment ahead of
|
|
118
|
+
* the video, so a stall shows as a stop, not as a bar that keeps going.
|
|
119
|
+
*/
|
|
120
|
+
const AHEAD_LIMIT = 0.25
|
|
121
|
+
let lastMedia = -1
|
|
122
|
+
let lastAt = 0
|
|
123
|
+
function smoothTime(video: HTMLVideoElement, now: number): number {
|
|
124
|
+
const t = video.currentTime
|
|
125
|
+
if (t !== lastMedia) {
|
|
126
|
+
lastMedia = t
|
|
127
|
+
lastAt = now
|
|
128
|
+
}
|
|
129
|
+
const estimate = Math.min(lastMedia + ((now - lastAt) / 1000) * video.playbackRate, lastMedia + AHEAD_LIMIT, duration.value || Infinity)
|
|
130
|
+
return Math.max(estimate, Math.min(time.value, lastMedia + AHEAD_LIMIT))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function tick(now: number) {
|
|
134
|
+
frame = 0
|
|
135
|
+
const video = el.value
|
|
136
|
+
if (!video || !playing.value) return
|
|
137
|
+
if (!seekBusy) time.value = smoothTime(video, now)
|
|
138
|
+
options.onFrame?.()
|
|
139
|
+
if (playing.value) frame = nodeWindow(video).requestAnimationFrame(tick)
|
|
140
|
+
}
|
|
141
|
+
function stopTicking() {
|
|
142
|
+
if (frame && el.value) nodeWindow(el.value).cancelAnimationFrame(frame)
|
|
143
|
+
frame = 0
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function readBuffered(video: HTMLVideoElement) {
|
|
147
|
+
const d = duration.value
|
|
148
|
+
if (!d) return
|
|
149
|
+
const out: [number, number][] = []
|
|
150
|
+
for (let i = 0; i < video.buffered.length; i++) out.push([video.buffered.start(i) / d, video.buffered.end(i) / d])
|
|
151
|
+
buffered.value = out
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function probe(src: string): Promise<VideoErrorCause> {
|
|
155
|
+
try {
|
|
156
|
+
const res = await fetch(src, { headers: { Range: "bytes=0-0" }, credentials: "same-origin" })
|
|
157
|
+
void res.body?.cancel()
|
|
158
|
+
const type = res.headers.get("content-type") ?? ""
|
|
159
|
+
return res.ok && !/^text\/|json|xml/.test(type) ? "unsupported" : "load"
|
|
160
|
+
} catch {
|
|
161
|
+
// Another origin without CORS: there is no telling, so the browser's word stands.
|
|
162
|
+
return "unsupported"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const listeners = {
|
|
167
|
+
loadedmetadata() {
|
|
168
|
+
const video = el.value
|
|
169
|
+
if (!video) return
|
|
170
|
+
size.value = { width: video.videoWidth, height: video.videoHeight }
|
|
171
|
+
if (applied !== options.src()) {
|
|
172
|
+
applied = options.src()
|
|
173
|
+
volume.value = sound.value.volume
|
|
174
|
+
rate.value = sound.value.rate
|
|
175
|
+
video.muted = video.muted || sound.value.muted
|
|
176
|
+
muted.value = video.muted
|
|
177
|
+
video.volume = volume.value
|
|
178
|
+
video.playbackRate = rate.value
|
|
179
|
+
}
|
|
180
|
+
if (Number.isFinite(video.duration)) {
|
|
181
|
+
duration.value = video.duration
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
probingDuration = true
|
|
185
|
+
video.currentTime = 1e101
|
|
186
|
+
},
|
|
187
|
+
durationchange() {
|
|
188
|
+
const video = el.value
|
|
189
|
+
if (!video || !Number.isFinite(video.duration) || !video.duration) return
|
|
190
|
+
duration.value = video.duration
|
|
191
|
+
if (probingDuration) {
|
|
192
|
+
probingDuration = false
|
|
193
|
+
video.currentTime = 0
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
seeked() {
|
|
197
|
+
seekBusy = false
|
|
198
|
+
lastMedia = -1
|
|
199
|
+
if (seekTarget !== null) {
|
|
200
|
+
const next = seekTarget
|
|
201
|
+
seekTarget = null
|
|
202
|
+
seek(next)
|
|
203
|
+
} else if (el.value && !playing.value) time.value = el.value.currentTime
|
|
204
|
+
},
|
|
205
|
+
play() {
|
|
206
|
+
lastMedia = -1
|
|
207
|
+
// Started from outside (media keys, the picture-in-picture window): it wants to play now.
|
|
208
|
+
wantsPlay.value = true
|
|
209
|
+
playing.value = true
|
|
210
|
+
ended.value = false
|
|
211
|
+
if (!frame && el.value) frame = nodeWindow(el.value).requestAnimationFrame(tick)
|
|
212
|
+
},
|
|
213
|
+
pause() {
|
|
214
|
+
wantsPlay.value = false
|
|
215
|
+
playing.value = false
|
|
216
|
+
stopTicking()
|
|
217
|
+
if (el.value && !seekBusy) time.value = el.value.currentTime
|
|
218
|
+
},
|
|
219
|
+
ended() {
|
|
220
|
+
instant = true
|
|
221
|
+
wantsPlay.value = false
|
|
222
|
+
ended.value = true
|
|
223
|
+
},
|
|
224
|
+
waiting() {
|
|
225
|
+
waiting.value = true
|
|
226
|
+
},
|
|
227
|
+
playing() {
|
|
228
|
+
waiting.value = false
|
|
229
|
+
},
|
|
230
|
+
canplay() {
|
|
231
|
+
waiting.value = false
|
|
232
|
+
},
|
|
233
|
+
timeupdate() {
|
|
234
|
+
if (el.value && !playing.value && !seekBusy) time.value = el.value.currentTime
|
|
235
|
+
},
|
|
236
|
+
progress() {
|
|
237
|
+
if (el.value) readBuffered(el.value)
|
|
238
|
+
},
|
|
239
|
+
volumechange() {
|
|
240
|
+
const video = el.value
|
|
241
|
+
if (!video) return
|
|
242
|
+
// Firefox reports itself muted below a quarter speed: during the easing that is the easing's.
|
|
243
|
+
if (!easing()) muted.value = video.muted
|
|
244
|
+
if (easing() || video.volume === written.volume) return
|
|
245
|
+
volume.value = video.volume
|
|
246
|
+
remember()
|
|
247
|
+
},
|
|
248
|
+
ratechange() {
|
|
249
|
+
const video = el.value
|
|
250
|
+
if (!video || easing() || video.playbackRate === written.rate) return
|
|
251
|
+
rate.value = video.playbackRate
|
|
252
|
+
remember()
|
|
253
|
+
},
|
|
254
|
+
async error() {
|
|
255
|
+
const video = el.value
|
|
256
|
+
if (!video?.error) return
|
|
257
|
+
const src = options.src()
|
|
258
|
+
let cause = mediaErrorCause(video.error)
|
|
259
|
+
if (cause === "unsupported") cause = await probe(src)
|
|
260
|
+
if (src !== options.src()) return
|
|
261
|
+
failure.value = cause
|
|
262
|
+
waiting.value = false
|
|
263
|
+
options.onError?.(cause)
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/*
|
|
268
|
+
* Start and stop are eased, as LpVideoBackdrop's `easePlayback` is: a play
|
|
269
|
+
* gathers speed and sound from a standstill over a moment, a pause slows and
|
|
270
|
+
* quietens to one, instead of the picture and the sound cutting in and out
|
|
271
|
+
* in a single frame (and the sound with a click). `wantsPlay` turns at once,
|
|
272
|
+
* so a button's icon answers the press; the element follows. A pause for a
|
|
273
|
+
* seek or a drag, or at a clip's end, is `instant`: there the exact frame
|
|
274
|
+
* matters more than the glide. Reduced motion has no easing at all.
|
|
275
|
+
*/
|
|
276
|
+
const wantsPlay = ref(false)
|
|
277
|
+
let instant = false
|
|
278
|
+
const reducedMotion = usePrefersReducedMotion()
|
|
279
|
+
const throttle = useTween<number>(
|
|
280
|
+
() => (wantsPlay.value ? 1 : 0),
|
|
281
|
+
(from, to, k) => from + (to - from) * k,
|
|
282
|
+
{ enabled: () => !instant && !reducedMotion.value && (options.ease?.() ?? true) },
|
|
283
|
+
)
|
|
284
|
+
/*
|
|
285
|
+
* Only around playback: a paused element keeps its real rate and volume.
|
|
286
|
+
* Firefox silences sound below a quarter speed and reports the element as
|
|
287
|
+
* muted meanwhile, so a paused video held at the slowest rate came up muted
|
|
288
|
+
* and would not be unmuted. While the rate eases, what the element reports
|
|
289
|
+
* about its sound is the easing's, not a choice, and is not taken as one.
|
|
290
|
+
*/
|
|
291
|
+
/* What the easing itself last wrote: the element's later report of the same value is an echo, not a choice. */
|
|
292
|
+
const written = { rate: Number.NaN, volume: Number.NaN }
|
|
293
|
+
function applyThrottle(video: HTMLVideoElement, k: number) {
|
|
294
|
+
written.rate = video.playbackRate = k >= 1 ? rate.value : Math.max(SLOWEST_RATE, rate.value * k)
|
|
295
|
+
written.volume = video.volume = k >= 1 ? volume.value : volume.value * k
|
|
296
|
+
}
|
|
297
|
+
watch(throttle, (k) => {
|
|
298
|
+
const video = el.value
|
|
299
|
+
if (!video) return
|
|
300
|
+
if (k === 0 && !wantsPlay.value) {
|
|
301
|
+
if (!video.paused) video.pause()
|
|
302
|
+
applyThrottle(video, 1)
|
|
303
|
+
instant = false
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
applyThrottle(video, k)
|
|
307
|
+
if (k === 1) instant = false
|
|
308
|
+
})
|
|
309
|
+
/*
|
|
310
|
+
* Whether the element's rate and volume are the easing's rather than the
|
|
311
|
+
* chosen ones: from the press of play until full speed, and from the press
|
|
312
|
+
* of pause until the stop. The element reports a change a moment after it
|
|
313
|
+
* is made, so the bounds are the intent, not the throttle alone: a start
|
|
314
|
+
* reported while the throttle still read 0 was taken for a choice of a
|
|
315
|
+
* sixteenth speed and silence.
|
|
316
|
+
*/
|
|
317
|
+
const easing = () => (wantsPlay.value ? throttle.value < 1 : throttle.value > 0)
|
|
318
|
+
|
|
319
|
+
function remember() {
|
|
320
|
+
if (applied) sound.value = { volume: volume.value, muted: muted.value, rate: rate.value }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Play, quietly: a browser that refuses (autoplay with sound) just leaves it paused. */
|
|
324
|
+
function play() {
|
|
325
|
+
const video = el.value
|
|
326
|
+
if (!video || failure.value) return
|
|
327
|
+
if (ended.value) seek(0)
|
|
328
|
+
wantsPlay.value = true
|
|
329
|
+
// From a standstill the first frame already plays slow and quiet, not at full speed for one frame.
|
|
330
|
+
if (!instant && !reducedMotion.value && (options.ease?.() ?? true) && throttle.value === 0) applyThrottle(video, 0)
|
|
331
|
+
void video.play().catch(() => {
|
|
332
|
+
wantsPlay.value = false
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
/** Pause, easing to a stop unless `instant` (a seek, a drag, a clip's end, where the frame must be exact). */
|
|
336
|
+
function pause(options: { instant?: boolean } = {}) {
|
|
337
|
+
if (options.instant) instant = true
|
|
338
|
+
wantsPlay.value = false
|
|
339
|
+
}
|
|
340
|
+
function toggle() {
|
|
341
|
+
if (wantsPlay.value) pause()
|
|
342
|
+
else play()
|
|
343
|
+
}
|
|
344
|
+
function setMuted(value: boolean) {
|
|
345
|
+
if (el.value) el.value.muted = value
|
|
346
|
+
muted.value = value
|
|
347
|
+
remember()
|
|
348
|
+
}
|
|
349
|
+
function setVolume(value: number) {
|
|
350
|
+
const v = Math.min(1, Math.max(0, value))
|
|
351
|
+
volume.value = v
|
|
352
|
+
if (el.value) {
|
|
353
|
+
el.value.volume = easing() ? v * throttle.value : v
|
|
354
|
+
// Turning the sound up is asking to hear it.
|
|
355
|
+
if (v > 0 && el.value.muted) el.value.muted = false
|
|
356
|
+
}
|
|
357
|
+
// Said now, not on the element's event: a slider bound to "muted ? 0 : volume" would spring back to 0 meanwhile.
|
|
358
|
+
if (v > 0) muted.value = false
|
|
359
|
+
remember()
|
|
360
|
+
}
|
|
361
|
+
function setRate(value: number) {
|
|
362
|
+
rate.value = value
|
|
363
|
+
if (el.value) el.value.playbackRate = easing() ? Math.max(SLOWEST_RATE, value * throttle.value) : value
|
|
364
|
+
remember()
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The frame on screen as a PNG, for "copy frame". Null when there is none
|
|
369
|
+
* yet, or when the video came from another origin without CORS and the
|
|
370
|
+
* browser will not let it be read back.
|
|
371
|
+
*/
|
|
372
|
+
function captureFrame(): Promise<Blob | null> {
|
|
373
|
+
const video = el.value
|
|
374
|
+
if (!video?.videoWidth) return Promise.resolve(null)
|
|
375
|
+
const canvas = video.ownerDocument.createElement("canvas")
|
|
376
|
+
canvas.width = video.videoWidth
|
|
377
|
+
canvas.height = video.videoHeight
|
|
378
|
+
try {
|
|
379
|
+
canvas.getContext("2d")?.drawImage(video, 0, 0)
|
|
380
|
+
return new Promise((resolve) => canvas.toBlob((blob) => resolve(blob), "image/png"))
|
|
381
|
+
} catch {
|
|
382
|
+
return Promise.resolve(null)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
watch(options.src, () => {
|
|
387
|
+
duration.value = 0
|
|
388
|
+
time.value = 0
|
|
389
|
+
failure.value = null
|
|
390
|
+
buffered.value = []
|
|
391
|
+
ended.value = false
|
|
392
|
+
waiting.value = false
|
|
393
|
+
seekBusy = false
|
|
394
|
+
seekTarget = null
|
|
395
|
+
probingDuration = false
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
onBeforeUnmount(() => {
|
|
399
|
+
stopTicking()
|
|
400
|
+
const video = el.value
|
|
401
|
+
if (!video) return
|
|
402
|
+
// Stop the stream from fetching on: an element that is merely detached keeps its connection.
|
|
403
|
+
video.pause()
|
|
404
|
+
video.removeAttribute("src")
|
|
405
|
+
video.load()
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
return { duration, time, playing, wantsPlay, waiting, ended, muted, volume, rate, buffered, failure, size, listeners, seek, play, pause, toggle, setMuted, setVolume, setRate, captureFrame }
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export type VideoMedia = ReturnType<typeof useVideoMedia>
|
package/src/index.ts
CHANGED
|
@@ -156,7 +156,7 @@ export { default as LpInput } from "./components/LpInput.vue"
|
|
|
156
156
|
export { default as LpLightbox } from "./components/LpLightbox.vue"
|
|
157
157
|
export type { LightboxLabels } from "./components/LpLightbox.vue"
|
|
158
158
|
export type { LightboxItem } from "./components/lightbox"
|
|
159
|
-
export { fileNameOf } from "./components/lightbox"
|
|
159
|
+
export { fileNameOf, isVideoItem } from "./components/lightbox"
|
|
160
160
|
export { default as LpLink } from "./components/LpLink.vue"
|
|
161
161
|
export { default as LpLogViewer } from "./components/LpLogViewer.vue"
|
|
162
162
|
export { default as LpLogInspector } from "./components/LpLogInspector.vue"
|
|
@@ -227,6 +227,9 @@ export { default as LpVideoBackdrop } from "./components/LpVideoBackdrop.vue"
|
|
|
227
227
|
export { formatClipTime, isNativeLoop, planLoop, seamProgress, seamReached } from "./components/videoBackdrop"
|
|
228
228
|
export type { LoopPlan } from "./components/videoBackdrop"
|
|
229
229
|
export { default as LpVideoTrim } from "./components/LpVideoTrim.vue"
|
|
230
|
+
export { default as LpVideoPlayer } from "./components/LpVideoPlayer.vue"
|
|
231
|
+
export { useVideoMedia } from "./composables/useVideoMedia"
|
|
232
|
+
export type { VideoErrorCause, VideoMedia } from "./composables/useVideoMedia"
|
|
230
233
|
export { dragEnd, normalizeRange as normalizeTrimRange, parseClipTime, setEnd } from "./components/videoTrim"
|
|
231
234
|
export type { FrameRegion, TimeRange, VideoTrimErrorCause } from "./components/videoTrim"
|
|
232
235
|
export { default as LpSparkline } from "./components/LpSparkline.vue"
|