@krizaka/ui 2.3.0 → 2.5.0
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/README.md +16 -1
- package/dist/audio-player.d.ts +113 -0
- package/dist/audio-player.js +207 -0
- package/dist/audio-player.js.map +1 -0
- package/dist/chunk-2USC2YT6.js +17 -0
- package/dist/chunk-2USC2YT6.js.map +1 -0
- package/dist/chunk-EUPTXKH7.js +95 -0
- package/dist/chunk-EUPTXKH7.js.map +1 -0
- package/dist/chunk-RUPRYJ45.js +82 -0
- package/dist/chunk-RUPRYJ45.js.map +1 -0
- package/dist/chunk-XUMJA5ZI.js +283 -0
- package/dist/chunk-XUMJA5ZI.js.map +1 -0
- package/dist/chunk-YH4LMWRU.js +92 -0
- package/dist/chunk-YH4LMWRU.js.map +1 -0
- package/dist/chunk-YX6CNVJA.js +94 -0
- package/dist/chunk-YX6CNVJA.js.map +1 -0
- package/dist/chunk-ZIXB33X7.js +95 -0
- package/dist/chunk-ZIXB33X7.js.map +1 -0
- package/dist/combobox.d.ts +188 -0
- package/dist/combobox.js +16 -0
- package/dist/combobox.js.map +1 -0
- package/dist/data-table.d.ts +246 -0
- package/dist/data-table.js +290 -0
- package/dist/data-table.js.map +1 -0
- package/dist/date-picker.d.ts +288 -0
- package/dist/date-picker.js +364 -0
- package/dist/date-picker.js.map +1 -0
- package/dist/file-upload.d.ts +238 -0
- package/dist/file-upload.js +437 -0
- package/dist/file-upload.js.map +1 -0
- package/dist/filter-bar.d.ts +139 -0
- package/dist/filter-bar.js +155 -0
- package/dist/filter-bar.js.map +1 -0
- package/dist/image-cropper.d.ts +115 -0
- package/dist/image-cropper.js +166 -0
- package/dist/image-cropper.js.map +1 -0
- package/dist/index.js.map +1 -1
- package/dist/media-capture.d.ts +155 -0
- package/dist/media-capture.js +242 -0
- package/dist/media-capture.js.map +1 -0
- package/dist/native/media-capture.d.ts +49 -0
- package/dist/native/media-capture.js +113 -0
- package/dist/native/media-capture.js.map +1 -0
- package/dist/native/video-player.d.ts +50 -0
- package/dist/native/video-player.js +69 -0
- package/dist/native/video-player.js.map +1 -0
- package/dist/native.d.ts +94 -3
- package/dist/native.js +195 -73
- package/dist/native.js.map +1 -1
- package/dist/pagination.d.ts +141 -0
- package/dist/pagination.js +165 -0
- package/dist/pagination.js.map +1 -0
- package/dist/search-field.d.ts +100 -0
- package/dist/search-field.js +167 -0
- package/dist/search-field.js.map +1 -0
- package/dist/tag-input.d.ts +44 -0
- package/dist/tag-input.js +67 -0
- package/dist/tag-input.js.map +1 -0
- package/dist/video-player.d.ts +204 -0
- package/dist/video-player.js +371 -0
- package/dist/video-player.js.map +1 -0
- package/dist/video-trimmer.d.ts +142 -0
- package/dist/video-trimmer.js +193 -0
- package/dist/video-trimmer.js.map +1 -0
- package/package.json +233 -5
- package/registry/alert.json +1 -1
- package/registry/audio-player.json +282 -0
- package/registry/avatar.json +1 -1
- package/registry/badge.json +1 -1
- package/registry/button.json +1 -1
- package/registry/card.json +1 -1
- package/registry/checkbox.json +1 -1
- package/registry/chip-link.json +1 -1
- package/registry/chip.json +1 -1
- package/registry/combobox.json +475 -0
- package/registry/command.json +1 -1
- package/registry/confirm-button.json +1 -1
- package/registry/countdown.json +1 -1
- package/registry/data-table.json +500 -0
- package/registry/date-picker.json +978 -0
- package/registry/dialog.json +1 -1
- package/registry/dropdown-menu.json +1 -1
- package/registry/empty-state.json +1 -1
- package/registry/examples/audio-player/podcast.d.ts +4 -0
- package/registry/examples/audio-player/podcast.js +22 -0
- package/registry/examples/audio-player/podcast.tsx +19 -0
- package/registry/examples/audio-player/voice-note.d.ts +4 -0
- package/registry/examples/audio-player/voice-note.js +12 -0
- package/registry/examples/audio-player/voice-note.tsx +12 -0
- package/registry/examples/combobox/async.d.ts +4 -0
- package/registry/examples/combobox/async.js +23 -0
- package/registry/examples/combobox/async.tsx +22 -0
- package/registry/examples/combobox/creatable.d.ts +4 -0
- package/registry/examples/combobox/creatable.js +32 -0
- package/registry/examples/combobox/creatable.tsx +28 -0
- package/registry/examples/combobox/invalid.d.ts +4 -0
- package/registry/examples/combobox/invalid.js +23 -0
- package/registry/examples/combobox/invalid.tsx +17 -0
- package/registry/examples/combobox/multiple.d.ts +4 -0
- package/registry/examples/combobox/multiple.js +29 -0
- package/registry/examples/combobox/multiple.tsx +26 -0
- package/registry/examples/data-table/cards.d.ts +4 -0
- package/registry/examples/data-table/cards.js +21 -0
- package/registry/examples/data-table/cards.tsx +20 -0
- package/registry/examples/data-table/empty.d.ts +4 -0
- package/registry/examples/data-table/empty.js +26 -0
- package/registry/examples/data-table/empty.tsx +21 -0
- package/registry/examples/data-table/error.d.ts +4 -0
- package/registry/examples/data-table/error.js +28 -0
- package/registry/examples/data-table/error.tsx +29 -0
- package/registry/examples/data-table/jobs.d.ts +4 -0
- package/registry/examples/data-table/jobs.js +37 -0
- package/registry/examples/data-table/jobs.tsx +36 -0
- package/registry/examples/data-table/loading.d.ts +4 -0
- package/registry/examples/data-table/loading.js +16 -0
- package/registry/examples/data-table/loading.tsx +12 -0
- package/registry/examples/data-table/virtualized.d.ts +4 -0
- package/registry/examples/data-table/virtualized.js +21 -0
- package/registry/examples/data-table/virtualized.tsx +20 -0
- package/registry/examples/date-picker/calendar.d.ts +4 -0
- package/registry/examples/date-picker/calendar.js +14 -0
- package/registry/examples/date-picker/calendar.tsx +16 -0
- package/registry/examples/date-picker/french.d.ts +4 -0
- package/registry/examples/date-picker/french.js +20 -0
- package/registry/examples/date-picker/french.tsx +14 -0
- package/registry/examples/date-picker/open.d.ts +4 -0
- package/registry/examples/date-picker/open.js +11 -0
- package/registry/examples/date-picker/open.tsx +8 -0
- package/registry/examples/date-picker/range.d.ts +4 -0
- package/registry/examples/date-picker/range.js +29 -0
- package/registry/examples/date-picker/range.tsx +25 -0
- package/registry/examples/date-picker/schedule.d.ts +4 -0
- package/registry/examples/date-picker/schedule.js +26 -0
- package/registry/examples/date-picker/schedule.tsx +22 -0
- package/registry/examples/file-upload/images.d.ts +4 -0
- package/registry/examples/file-upload/images.js +32 -0
- package/registry/examples/file-upload/images.tsx +25 -0
- package/registry/examples/file-upload/states.d.ts +4 -0
- package/registry/examples/file-upload/states.js +28 -0
- package/registry/examples/file-upload/states.tsx +23 -0
- package/registry/examples/file-upload/tus.d.ts +4 -0
- package/registry/examples/file-upload/tus.js +23 -0
- package/registry/examples/file-upload/tus.tsx +26 -0
- package/registry/examples/file-upload/video.d.ts +4 -0
- package/registry/examples/file-upload/video.js +39 -0
- package/registry/examples/file-upload/video.tsx +36 -0
- package/registry/examples/filter-bar/address.d.ts +4 -0
- package/registry/examples/filter-bar/address.js +29 -0
- package/registry/examples/filter-bar/address.tsx +27 -0
- package/registry/examples/filter-bar/jobs.d.ts +4 -0
- package/registry/examples/filter-bar/jobs.js +37 -0
- package/registry/examples/filter-bar/jobs.tsx +37 -0
- package/registry/examples/filter-bar/menu.d.ts +4 -0
- package/registry/examples/filter-bar/menu.js +27 -0
- package/registry/examples/filter-bar/menu.tsx +23 -0
- package/registry/examples/image-cropper/avatar.d.ts +4 -0
- package/registry/examples/image-cropper/avatar.js +13 -0
- package/registry/examples/image-cropper/avatar.tsx +14 -0
- package/registry/examples/image-cropper/banner.d.ts +4 -0
- package/registry/examples/image-cropper/banner.js +11 -0
- package/registry/examples/image-cropper/banner.tsx +10 -0
- package/registry/examples/image-cropper/vertical-video.d.ts +4 -0
- package/registry/examples/image-cropper/vertical-video.js +12 -0
- package/registry/examples/image-cropper/vertical-video.tsx +16 -0
- package/registry/examples/media-capture/clip.d.ts +4 -0
- package/registry/examples/media-capture/clip.js +20 -0
- package/registry/examples/media-capture/clip.tsx +18 -0
- package/registry/examples/media-capture/native/photo.d.ts +4 -0
- package/registry/examples/media-capture/native/photo.dark.png +0 -0
- package/registry/examples/media-capture/native/photo.js +27 -0
- package/registry/examples/media-capture/native/photo.light.png +0 -0
- package/registry/examples/media-capture/native/photo.tsx +20 -0
- package/registry/examples/media-capture/profile-photo.d.ts +4 -0
- package/registry/examples/media-capture/profile-photo.js +24 -0
- package/registry/examples/media-capture/profile-photo.tsx +20 -0
- package/registry/examples/pagination/cursor.d.ts +4 -0
- package/registry/examples/pagination/cursor.js +24 -0
- package/registry/examples/pagination/cursor.tsx +20 -0
- package/registry/examples/pagination/links.d.ts +4 -0
- package/registry/examples/pagination/links.js +11 -0
- package/registry/examples/pagination/links.tsx +6 -0
- package/registry/examples/pagination/native/cursor.d.ts +4 -0
- package/registry/examples/pagination/native/cursor.dark.png +0 -0
- package/registry/examples/pagination/native/cursor.js +19 -0
- package/registry/examples/pagination/native/cursor.light.png +0 -0
- package/registry/examples/pagination/native/cursor.tsx +13 -0
- package/registry/examples/pagination/native/pages.d.ts +4 -0
- package/registry/examples/pagination/native/pages.dark.png +0 -0
- package/registry/examples/pagination/native/pages.js +21 -0
- package/registry/examples/pagination/native/pages.light.png +0 -0
- package/registry/examples/pagination/native/pages.tsx +14 -0
- package/registry/examples/pagination/results.d.ts +4 -0
- package/registry/examples/pagination/results.js +23 -0
- package/registry/examples/pagination/results.tsx +19 -0
- package/registry/examples/pagination/table-footer.d.ts +4 -0
- package/registry/examples/pagination/table-footer.js +31 -0
- package/registry/examples/pagination/table-footer.tsx +27 -0
- package/registry/examples/search-field/global.d.ts +4 -0
- package/registry/examples/search-field/global.js +19 -0
- package/registry/examples/search-field/global.tsx +15 -0
- package/registry/examples/search-field/native/default.d.ts +4 -0
- package/registry/examples/search-field/native/default.dark.png +0 -0
- package/registry/examples/search-field/native/default.js +13 -0
- package/registry/examples/search-field/native/default.light.png +0 -0
- package/registry/examples/search-field/native/default.tsx +7 -0
- package/registry/examples/search-field/suggestions.d.ts +4 -0
- package/registry/examples/search-field/suggestions.js +30 -0
- package/registry/examples/search-field/suggestions.tsx +27 -0
- package/registry/examples/search-field/toolbar.d.ts +4 -0
- package/registry/examples/search-field/toolbar.js +17 -0
- package/registry/examples/search-field/toolbar.tsx +21 -0
- package/registry/examples/tag-input/recipients.d.ts +4 -0
- package/registry/examples/tag-input/recipients.js +23 -0
- package/registry/examples/tag-input/recipients.tsx +20 -0
- package/registry/examples/tag-input/video-tags.d.ts +4 -0
- package/registry/examples/tag-input/video-tags.js +26 -0
- package/registry/examples/tag-input/video-tags.tsx +22 -0
- package/registry/examples/video-player/default.d.ts +4 -0
- package/registry/examples/video-player/default.js +12 -0
- package/registry/examples/video-player/default.tsx +8 -0
- package/registry/examples/video-player/gated.d.ts +4 -0
- package/registry/examples/video-player/gated.js +26 -0
- package/registry/examples/video-player/gated.tsx +23 -0
- package/registry/examples/video-player/native/default.d.ts +4 -0
- package/registry/examples/video-player/native/default.dark.png +0 -0
- package/registry/examples/video-player/native/default.js +18 -0
- package/registry/examples/video-player/native/default.light.png +0 -0
- package/registry/examples/video-player/native/default.tsx +12 -0
- package/registry/examples/video-player/signed-source.d.ts +4 -0
- package/registry/examples/video-player/signed-source.js +16 -0
- package/registry/examples/video-player/signed-source.tsx +15 -0
- package/registry/examples/video-player/vertical.d.ts +4 -0
- package/registry/examples/video-player/vertical.js +12 -0
- package/registry/examples/video-player/vertical.tsx +12 -0
- package/registry/examples/video-trimmer/custom.d.ts +4 -0
- package/registry/examples/video-trimmer/custom.js +24 -0
- package/registry/examples/video-trimmer/custom.tsx +26 -0
- package/registry/examples/video-trimmer/filmstrip.d.ts +4 -0
- package/registry/examples/video-trimmer/filmstrip.js +15 -0
- package/registry/examples/video-trimmer/filmstrip.tsx +13 -0
- package/registry/examples/video-trimmer/playhead.d.ts +4 -0
- package/registry/examples/video-trimmer/playhead.js +15 -0
- package/registry/examples/video-trimmer/playhead.tsx +13 -0
- package/registry/field.json +1 -1
- package/registry/file-upload.json +471 -0
- package/registry/filter-bar.json +347 -0
- package/registry/glow-card.json +1 -1
- package/registry/image-cropper.json +313 -0
- package/registry/index.json +255 -1
- package/registry/iso.json +1 -1
- package/registry/kbd.json +1 -1
- package/registry/loader.json +1 -1
- package/registry/marquee.json +1 -1
- package/registry/mascots.json +1 -1
- package/registry/media-capture.json +347 -0
- package/registry/meter.json +3 -3
- package/registry/nav-bar.json +1 -1
- package/registry/number-ticker.json +1 -1
- package/registry/page-header.json +1 -1
- package/registry/pagination.json +475 -0
- package/registry/popover.json +1 -1
- package/registry/progress.json +5 -5
- package/registry/radio-group.json +1 -1
- package/registry/search-field.json +436 -0
- package/registry/section-backdrop.json +1 -1
- package/registry/separator.json +1 -1
- package/registry/skeleton.json +1 -1
- package/registry/slider.json +1 -1
- package/registry/slot.json +1 -1
- package/registry/spinner.json +1 -1
- package/registry/stat.json +1 -1
- package/registry/stepper.json +1 -1
- package/registry/switch.json +1 -1
- package/registry/tabs.json +1 -1
- package/registry/tag-input.json +363 -0
- package/registry/theme.json +1 -1
- package/registry/toast.json +1 -1
- package/registry/tooltip.json +1 -1
- package/registry/video-player.json +598 -0
- package/registry/video-trimmer.json +385 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
formatClock,
|
|
4
|
+
playSafely,
|
|
5
|
+
usePrefersReducedMotion
|
|
6
|
+
} from "./chunk-EUPTXKH7.js";
|
|
7
|
+
import {
|
|
8
|
+
CameraGlyph,
|
|
9
|
+
FlipGlyph
|
|
10
|
+
} from "./chunk-YX6CNVJA.js";
|
|
11
|
+
|
|
12
|
+
// src/media-capture/media-capture.tsx
|
|
13
|
+
import * as React from "react";
|
|
14
|
+
import { tv } from "tailwind-variants";
|
|
15
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
16
|
+
var mediaCapture = tv({
|
|
17
|
+
slots: {
|
|
18
|
+
root: "theme-dark relative isolate flex w-full flex-col overflow-hidden rounded-xl bg-media text-fg-on-media",
|
|
19
|
+
stage: "relative w-full",
|
|
20
|
+
video: "absolute inset-0 h-full w-full object-cover data-[mirrored]:-scale-x-100",
|
|
21
|
+
panel: "absolute inset-0 flex flex-col items-center justify-center gap-3 p-6 text-center text-sm",
|
|
22
|
+
panelIcon: "flex h-12 w-12 items-center justify-center rounded-full bg-fg-on-media/15",
|
|
23
|
+
primary: "inline-flex h-10 items-center justify-center gap-2 rounded-lg bg-accent px-4 text-sm font-semibold text-on-accent hover:bg-accent-hover focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-media",
|
|
24
|
+
secondary: "inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-fg-on-media/40 px-4 text-sm font-semibold text-fg-on-media hover:bg-fg-on-media/10 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring",
|
|
25
|
+
bar: "absolute inset-x-0 bottom-0 flex items-center justify-center gap-6 bg-linear-to-t from-scrim-strong to-transparent px-4 pb-4 pt-10",
|
|
26
|
+
shutter: "relative inline-flex h-16 w-16 items-center justify-center rounded-full border-4 border-fg-on-media bg-transparent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-media after:h-12 after:w-12 after:rounded-full after:bg-fg-on-media after:transition-all motion-reduce:after:transition-none data-[mode=video]:after:bg-danger data-[recording]:after:h-6 data-[recording]:after:w-6 data-[recording]:after:rounded-md",
|
|
27
|
+
round: "inline-flex h-11 w-11 items-center justify-center rounded-full bg-scrim-strong text-fg-on-media hover:bg-scrim focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:invisible",
|
|
28
|
+
modes: "absolute inset-x-0 top-3 flex justify-center",
|
|
29
|
+
mode: "rounded-full px-3 py-1 text-xs font-semibold text-fg-on-media/80 aria-pressed:bg-fg-on-media aria-pressed:text-media focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring",
|
|
30
|
+
timer: "absolute left-3 top-3 inline-flex items-center gap-1.5 rounded-full bg-scrim-strong px-2.5 py-1 font-mono text-xs tabular-nums before:h-2 before:w-2 before:rounded-full before:bg-danger before:animate-pulse motion-reduce:before:animate-none"
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
var mediaCaptureLabels = {
|
|
34
|
+
region: "Camera",
|
|
35
|
+
prompt: "Use your camera to take a photo or record a clip.",
|
|
36
|
+
start: "Turn on the camera",
|
|
37
|
+
denied: "Camera access is blocked. Allow it in your browser settings, or choose a file.",
|
|
38
|
+
unavailable: "No camera is available here.",
|
|
39
|
+
fallback: "Choose a file",
|
|
40
|
+
takePhoto: "Take photo",
|
|
41
|
+
startRecording: "Start recording",
|
|
42
|
+
stopRecording: "Stop recording",
|
|
43
|
+
switchCamera: "Switch camera",
|
|
44
|
+
photoMode: "Photo",
|
|
45
|
+
videoMode: "Video"
|
|
46
|
+
};
|
|
47
|
+
var supported = () => typeof navigator !== "undefined" && Boolean(navigator.mediaDevices?.getUserMedia);
|
|
48
|
+
var recorderType = () => typeof MediaRecorder === "undefined" ? void 0 : ["video/webm;codecs=vp9,opus", "video/webm", "video/mp4"].find((t) => MediaRecorder.isTypeSupported?.(t));
|
|
49
|
+
function MediaCapture({
|
|
50
|
+
mode = "photo",
|
|
51
|
+
facing: initialFacing = "user",
|
|
52
|
+
audio = true,
|
|
53
|
+
maxDuration,
|
|
54
|
+
autoStart,
|
|
55
|
+
onCapture,
|
|
56
|
+
onError,
|
|
57
|
+
aspectRatio = "3 / 4",
|
|
58
|
+
labels: given,
|
|
59
|
+
className
|
|
60
|
+
}) {
|
|
61
|
+
const labels = { ...mediaCaptureLabels, ...given };
|
|
62
|
+
const s = mediaCapture();
|
|
63
|
+
const reduce = usePrefersReducedMotion();
|
|
64
|
+
const video = React.useRef(null);
|
|
65
|
+
const stream = React.useRef(null);
|
|
66
|
+
const recorder = React.useRef(null);
|
|
67
|
+
const file = React.useRef(null);
|
|
68
|
+
const [state, setState] = React.useState("idle");
|
|
69
|
+
const [facing, setFacing] = React.useState(initialFacing);
|
|
70
|
+
const [kind, setKind] = React.useState(mode === "video" ? "video" : "photo");
|
|
71
|
+
const [recording, setRecording] = React.useState(false);
|
|
72
|
+
const [elapsed, setElapsed] = React.useState(0);
|
|
73
|
+
const [cameras, setCameras] = React.useState(0);
|
|
74
|
+
const stop = React.useCallback(() => {
|
|
75
|
+
if (recorder.current?.state === "recording") recorder.current.stop();
|
|
76
|
+
stream.current?.getTracks().forEach((track) => track.stop());
|
|
77
|
+
stream.current = null;
|
|
78
|
+
}, []);
|
|
79
|
+
const start = React.useCallback(
|
|
80
|
+
async (side) => {
|
|
81
|
+
if (!supported()) {
|
|
82
|
+
setState("unavailable");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
setState("starting");
|
|
86
|
+
stop();
|
|
87
|
+
try {
|
|
88
|
+
const media = await navigator.mediaDevices.getUserMedia({ video: { facingMode: side }, audio: mode !== "photo" && audio });
|
|
89
|
+
stream.current = media;
|
|
90
|
+
if (video.current) {
|
|
91
|
+
video.current.srcObject = media;
|
|
92
|
+
playSafely(video.current);
|
|
93
|
+
}
|
|
94
|
+
const devices = await navigator.mediaDevices.enumerateDevices?.().catch(() => []);
|
|
95
|
+
setCameras((devices ?? []).filter((d) => d.kind === "videoinput").length);
|
|
96
|
+
setState("live");
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const name = error?.name;
|
|
99
|
+
setState(name === "NotAllowedError" || name === "SecurityError" ? "denied" : "unavailable");
|
|
100
|
+
onError?.(error);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
[audio, mode, onError, stop]
|
|
104
|
+
);
|
|
105
|
+
React.useEffect(() => {
|
|
106
|
+
if (!autoStart || !supported() || !navigator.permissions?.query) return;
|
|
107
|
+
let live = true;
|
|
108
|
+
navigator.permissions.query({ name: "camera" }).then((status) => {
|
|
109
|
+
if (live && status.state === "granted") void start(initialFacing);
|
|
110
|
+
}).catch(() => void 0);
|
|
111
|
+
return () => {
|
|
112
|
+
live = false;
|
|
113
|
+
};
|
|
114
|
+
}, [autoStart, initialFacing, start]);
|
|
115
|
+
React.useEffect(() => {
|
|
116
|
+
const hide = () => {
|
|
117
|
+
if (!document.hidden) return;
|
|
118
|
+
stop();
|
|
119
|
+
setState((current) => current === "live" ? "idle" : current);
|
|
120
|
+
};
|
|
121
|
+
document.addEventListener("visibilitychange", hide);
|
|
122
|
+
return () => {
|
|
123
|
+
document.removeEventListener("visibilitychange", hide);
|
|
124
|
+
stop();
|
|
125
|
+
};
|
|
126
|
+
}, [stop]);
|
|
127
|
+
React.useEffect(() => {
|
|
128
|
+
if (!recording) return;
|
|
129
|
+
const began = Date.now();
|
|
130
|
+
const timer = setInterval(() => {
|
|
131
|
+
const seconds = (Date.now() - began) / 1e3;
|
|
132
|
+
setElapsed(seconds);
|
|
133
|
+
if (maxDuration && seconds >= maxDuration) recorder.current?.stop();
|
|
134
|
+
}, 250);
|
|
135
|
+
return () => clearInterval(timer);
|
|
136
|
+
}, [recording, maxDuration]);
|
|
137
|
+
const deliver = (blob, captured) => onCapture({ blob, kind: captured, url: URL.createObjectURL(blob) });
|
|
138
|
+
const takePhoto = () => {
|
|
139
|
+
const element = video.current;
|
|
140
|
+
if (!element?.videoWidth) return;
|
|
141
|
+
const canvas = document.createElement("canvas");
|
|
142
|
+
canvas.width = element.videoWidth;
|
|
143
|
+
canvas.height = element.videoHeight;
|
|
144
|
+
const context = canvas.getContext("2d");
|
|
145
|
+
if (!context) return;
|
|
146
|
+
if (facing === "user") {
|
|
147
|
+
context.translate(canvas.width, 0);
|
|
148
|
+
context.scale(-1, 1);
|
|
149
|
+
}
|
|
150
|
+
context.drawImage(element, 0, 0);
|
|
151
|
+
canvas.toBlob((blob) => blob && deliver(blob, "photo"), "image/jpeg", 0.92);
|
|
152
|
+
};
|
|
153
|
+
const toggleRecording = () => {
|
|
154
|
+
if (recording) {
|
|
155
|
+
recorder.current?.stop();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (!stream.current) return;
|
|
159
|
+
const chunks = [];
|
|
160
|
+
const type = recorderType();
|
|
161
|
+
const next = new MediaRecorder(stream.current, type ? { mimeType: type } : void 0);
|
|
162
|
+
next.ondataavailable = (event) => event.data.size && chunks.push(event.data);
|
|
163
|
+
next.onstop = () => {
|
|
164
|
+
setRecording(false);
|
|
165
|
+
setElapsed(0);
|
|
166
|
+
deliver(new Blob(chunks, { type: next.mimeType || type || "video/webm" }), "video");
|
|
167
|
+
};
|
|
168
|
+
recorder.current = next;
|
|
169
|
+
next.start(1e3);
|
|
170
|
+
setRecording(true);
|
|
171
|
+
};
|
|
172
|
+
const flip = () => {
|
|
173
|
+
const side = facing === "user" ? "environment" : "user";
|
|
174
|
+
setFacing(side);
|
|
175
|
+
void start(side);
|
|
176
|
+
};
|
|
177
|
+
const accept = mode === "video" ? "video/*" : mode === "photo" ? "image/*" : "image/*,video/*";
|
|
178
|
+
const fallback = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
179
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: s.secondary(), onClick: () => file.current?.click(), children: labels.fallback }),
|
|
180
|
+
/* @__PURE__ */ jsx(
|
|
181
|
+
"input",
|
|
182
|
+
{
|
|
183
|
+
ref: file,
|
|
184
|
+
type: "file",
|
|
185
|
+
accept,
|
|
186
|
+
capture: facing,
|
|
187
|
+
tabIndex: -1,
|
|
188
|
+
"aria-hidden": true,
|
|
189
|
+
className: "sr-only",
|
|
190
|
+
onChange: (event) => {
|
|
191
|
+
const picked = event.target.files?.[0];
|
|
192
|
+
if (picked) deliver(picked, picked.type.startsWith("video/") ? "video" : "photo");
|
|
193
|
+
event.target.value = "";
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
] });
|
|
198
|
+
return /* @__PURE__ */ jsx("section", { "aria-label": labels.region, className: s.root({ className }), children: /* @__PURE__ */ jsxs("div", { className: s.stage(), style: { aspectRatio }, children: [
|
|
199
|
+
/* @__PURE__ */ jsx("video", { ref: video, muted: true, playsInline: true, "aria-hidden": true, "data-mirrored": facing === "user" ? "" : void 0, className: s.video(), hidden: state !== "live" }),
|
|
200
|
+
state !== "live" && /* @__PURE__ */ jsxs("div", { className: s.panel(), role: state === "denied" || state === "unavailable" ? "alert" : void 0, children: [
|
|
201
|
+
/* @__PURE__ */ jsx("span", { className: s.panelIcon(), "aria-hidden": true, children: /* @__PURE__ */ jsx(CameraGlyph, { className: "h-6 w-6" }) }),
|
|
202
|
+
/* @__PURE__ */ jsx("p", { className: "max-w-xs", children: state === "denied" ? labels.denied : state === "unavailable" ? labels.unavailable : labels.prompt }),
|
|
203
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap justify-center gap-2", children: [
|
|
204
|
+
(state === "idle" || state === "starting") && /* @__PURE__ */ jsxs("button", { type: "button", className: s.primary(), disabled: state === "starting", onClick: () => void start(facing), children: [
|
|
205
|
+
/* @__PURE__ */ jsx(CameraGlyph, { className: "h-4 w-4" }),
|
|
206
|
+
labels.start
|
|
207
|
+
] }),
|
|
208
|
+
fallback
|
|
209
|
+
] })
|
|
210
|
+
] }),
|
|
211
|
+
state === "live" && mode === "both" && !recording && /* @__PURE__ */ jsx("div", { className: s.modes(), children: /* @__PURE__ */ jsxs("div", { className: "flex gap-1 rounded-full bg-scrim-strong p-1", children: [
|
|
212
|
+
/* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": kind === "photo", className: s.mode(), onClick: () => setKind("photo"), children: labels.photoMode }),
|
|
213
|
+
/* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": kind === "video", className: s.mode(), onClick: () => setKind("video"), children: labels.videoMode })
|
|
214
|
+
] }) }),
|
|
215
|
+
recording && /* @__PURE__ */ jsxs("span", { className: s.timer(), role: "timer", "aria-live": "off", children: [
|
|
216
|
+
formatClock(elapsed),
|
|
217
|
+
maxDuration ? ` / ${formatClock(maxDuration)}` : ""
|
|
218
|
+
] }),
|
|
219
|
+
state === "live" && /* @__PURE__ */ jsxs("div", { className: s.bar(), children: [
|
|
220
|
+
/* @__PURE__ */ jsx("span", { className: "h-11 w-11" }),
|
|
221
|
+
/* @__PURE__ */ jsx(
|
|
222
|
+
"button",
|
|
223
|
+
{
|
|
224
|
+
type: "button",
|
|
225
|
+
className: s.shutter(),
|
|
226
|
+
"data-mode": kind,
|
|
227
|
+
"data-recording": recording ? "" : void 0,
|
|
228
|
+
"data-reduce": reduce ? "" : void 0,
|
|
229
|
+
"aria-label": kind === "photo" ? labels.takePhoto : recording ? labels.stopRecording : labels.startRecording,
|
|
230
|
+
onClick: kind === "photo" ? takePhoto : toggleRecording
|
|
231
|
+
}
|
|
232
|
+
),
|
|
233
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: s.round(), "aria-label": labels.switchCamera, disabled: cameras < 2 || recording, onClick: flip, children: /* @__PURE__ */ jsx(FlipGlyph, { className: "h-5 w-5" }) })
|
|
234
|
+
] })
|
|
235
|
+
] }) });
|
|
236
|
+
}
|
|
237
|
+
export {
|
|
238
|
+
MediaCapture,
|
|
239
|
+
mediaCapture,
|
|
240
|
+
mediaCaptureLabels
|
|
241
|
+
};
|
|
242
|
+
//# sourceMappingURL=media-capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/media-capture/media-capture.tsx"],"sourcesContent":["// Client. The camera in the page: asks for it only when the person presses the button (never on load), shows the\n// preview (mirrored for the front camera), takes a photo (a canvas → JPEG `Blob`) or records a clip (MediaRecorder,\n// a timer, a maximum length), switches cameras. Every dead end has a way out: permission refused, no camera, no\n// `getUserMedia` (an old browser, an insecure page) → the file picker with `capture`, which opens the phone's own\n// camera. The tracks stop when it unmounts or the page is hidden.\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { CameraGlyph, FlipGlyph } from \"../glyphs\";\nimport { formatClock, playSafely, usePrefersReducedMotion } from \"../media\";\n\nexport const mediaCapture = tv({\n slots: {\n root: \"theme-dark relative isolate flex w-full flex-col overflow-hidden rounded-xl bg-media text-fg-on-media\",\n stage: \"relative w-full\",\n video: \"absolute inset-0 h-full w-full object-cover data-[mirrored]:-scale-x-100\",\n panel: \"absolute inset-0 flex flex-col items-center justify-center gap-3 p-6 text-center text-sm\",\n panelIcon: \"flex h-12 w-12 items-center justify-center rounded-full bg-fg-on-media/15\",\n primary:\n \"inline-flex h-10 items-center justify-center gap-2 rounded-lg bg-accent px-4 text-sm font-semibold text-on-accent hover:bg-accent-hover \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-media\",\n secondary:\n \"inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-fg-on-media/40 px-4 text-sm font-semibold text-fg-on-media hover:bg-fg-on-media/10 \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n bar: \"absolute inset-x-0 bottom-0 flex items-center justify-center gap-6 bg-linear-to-t from-scrim-strong to-transparent px-4 pb-4 pt-10\",\n shutter:\n \"relative inline-flex h-16 w-16 items-center justify-center rounded-full border-4 border-fg-on-media bg-transparent \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-media \" +\n \"after:h-12 after:w-12 after:rounded-full after:bg-fg-on-media after:transition-all motion-reduce:after:transition-none \" +\n \"data-[mode=video]:after:bg-danger data-[recording]:after:h-6 data-[recording]:after:w-6 data-[recording]:after:rounded-md\",\n round:\n \"inline-flex h-11 w-11 items-center justify-center rounded-full bg-scrim-strong text-fg-on-media hover:bg-scrim \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:invisible\",\n modes: \"absolute inset-x-0 top-3 flex justify-center\",\n mode:\n \"rounded-full px-3 py-1 text-xs font-semibold text-fg-on-media/80 aria-pressed:bg-fg-on-media aria-pressed:text-media \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n timer:\n \"absolute left-3 top-3 inline-flex items-center gap-1.5 rounded-full bg-scrim-strong px-2.5 py-1 font-mono text-xs tabular-nums \" +\n \"before:h-2 before:w-2 before:rounded-full before:bg-danger before:animate-pulse motion-reduce:before:animate-none\",\n },\n});\n\nexport type CaptureResult = {\n /** The photo (JPEG) or the clip (WebM / MP4, as the browser records). */\n blob: Blob;\n /** `photo` or `video`. */\n kind: \"photo\" | \"video\";\n /** An object URL of the blob, to preview it (revoke it when done). */\n url: string;\n};\n\nexport type MediaCaptureLabels = {\n /** The accessible name of the camera (\"Camera\"). */\n region: string;\n /** Why the camera is asked for, before the request (\"Use your camera to take a photo\"). */\n prompt: string;\n /** The button that asks (\"Turn on the camera\"). */\n start: string;\n /** Permission refused (\"Camera access is blocked. Allow it in your browser settings, or choose a file.\"). */\n denied: string;\n /** No camera, or no access possible here (\"No camera is available.\"). */\n unavailable: string;\n /** The way out: the file picker (\"Choose a file\"). */\n fallback: string;\n takePhoto: string;\n startRecording: string;\n stopRecording: string;\n switchCamera: string;\n photoMode: string;\n videoMode: string;\n};\n\nexport const mediaCaptureLabels: MediaCaptureLabels = {\n region: \"Camera\",\n prompt: \"Use your camera to take a photo or record a clip.\",\n start: \"Turn on the camera\",\n denied: \"Camera access is blocked. Allow it in your browser settings, or choose a file.\",\n unavailable: \"No camera is available here.\",\n fallback: \"Choose a file\",\n takePhoto: \"Take photo\",\n startRecording: \"Start recording\",\n stopRecording: \"Stop recording\",\n switchCamera: \"Switch camera\",\n photoMode: \"Photo\",\n videoMode: \"Video\",\n};\n\nexport type MediaCaptureProps = {\n /** What it captures: `photo` (default), `video`, or `both` (a switch above the preview). */\n mode?: \"photo\" | \"video\" | \"both\";\n /** The camera at first: `user` (front, mirrored, default) or `environment` (back). */\n facing?: \"user\" | \"environment\";\n /** Records the microphone with a clip. Default `true`. */\n audio?: boolean;\n /** The longest clip, in seconds: recording stops by itself. */\n maxDuration?: number;\n /** Starts the camera at mount (only when the permission is already granted — never a prompt on load). */\n autoStart?: boolean;\n /** Called with a photo taken or a clip recorded (or a file picked in the fallback). */\n onCapture: (result: CaptureResult) => void;\n /** Called when the camera cannot start (`NotAllowedError`, `NotFoundError`…). */\n onError?: (error: unknown) => void;\n /** The width / height ratio of the preview. Default `\"3 / 4\"`. */\n aspectRatio?: string;\n /** The words, passed translated; English by default. */\n labels?: Partial<MediaCaptureLabels>;\n /** Classes merged on the root. */\n className?: string;\n};\n\ntype State = \"idle\" | \"starting\" | \"live\" | \"denied\" | \"unavailable\";\n\nconst supported = () => typeof navigator !== \"undefined\" && Boolean(navigator.mediaDevices?.getUserMedia);\nconst recorderType = () =>\n typeof MediaRecorder === \"undefined\" ? undefined : [\"video/webm;codecs=vp9,opus\", \"video/webm\", \"video/mp4\"].find((t) => MediaRecorder.isTypeSupported?.(t));\n\n/** The camera in the page: photo or clip, front or back, with a file-picker way out when the camera cannot start. */\nexport function MediaCapture({\n mode = \"photo\",\n facing: initialFacing = \"user\",\n audio = true,\n maxDuration,\n autoStart,\n onCapture,\n onError,\n aspectRatio = \"3 / 4\",\n labels: given,\n className,\n}: MediaCaptureProps) {\n const labels = { ...mediaCaptureLabels, ...given };\n const s = mediaCapture();\n const reduce = usePrefersReducedMotion();\n const video = React.useRef<HTMLVideoElement>(null);\n const stream = React.useRef<MediaStream | null>(null);\n const recorder = React.useRef<MediaRecorder | null>(null);\n const file = React.useRef<HTMLInputElement>(null);\n const [state, setState] = React.useState<State>(\"idle\");\n const [facing, setFacing] = React.useState(initialFacing);\n const [kind, setKind] = React.useState<\"photo\" | \"video\">(mode === \"video\" ? \"video\" : \"photo\");\n const [recording, setRecording] = React.useState(false);\n const [elapsed, setElapsed] = React.useState(0);\n const [cameras, setCameras] = React.useState(0);\n\n const stop = React.useCallback(() => {\n if (recorder.current?.state === \"recording\") recorder.current.stop();\n stream.current?.getTracks().forEach((track) => track.stop());\n stream.current = null;\n }, []);\n\n const start = React.useCallback(\n async (side: \"user\" | \"environment\") => {\n if (!supported()) {\n setState(\"unavailable\");\n return;\n }\n setState(\"starting\");\n stop();\n try {\n const media = await navigator.mediaDevices.getUserMedia({ video: { facingMode: side }, audio: mode !== \"photo\" && audio });\n stream.current = media;\n if (video.current) {\n video.current.srcObject = media;\n playSafely(video.current);\n }\n const devices = await navigator.mediaDevices.enumerateDevices?.().catch(() => []);\n setCameras((devices ?? []).filter((d) => d.kind === \"videoinput\").length);\n setState(\"live\");\n } catch (error) {\n const name = (error as { name?: string })?.name;\n setState(name === \"NotAllowedError\" || name === \"SecurityError\" ? \"denied\" : \"unavailable\");\n onError?.(error);\n }\n },\n [audio, mode, onError, stop],\n );\n\n React.useEffect(() => {\n if (!autoStart || !supported() || !navigator.permissions?.query) return;\n let live = true;\n navigator.permissions\n .query({ name: \"camera\" as PermissionName })\n .then((status) => {\n if (live && status.state === \"granted\") void start(initialFacing);\n })\n .catch(() => undefined);\n return () => {\n live = false;\n };\n }, [autoStart, initialFacing, start]);\n\n React.useEffect(() => {\n const hide = () => {\n if (!document.hidden) return;\n stop();\n setState((current) => (current === \"live\" ? \"idle\" : current));\n };\n document.addEventListener(\"visibilitychange\", hide);\n return () => {\n document.removeEventListener(\"visibilitychange\", hide);\n stop();\n };\n }, [stop]);\n\n React.useEffect(() => {\n if (!recording) return;\n const began = Date.now();\n const timer = setInterval(() => {\n const seconds = (Date.now() - began) / 1000;\n setElapsed(seconds);\n if (maxDuration && seconds >= maxDuration) recorder.current?.stop();\n }, 250);\n return () => clearInterval(timer);\n }, [recording, maxDuration]);\n\n const deliver = (blob: Blob, captured: \"photo\" | \"video\") => onCapture({ blob, kind: captured, url: URL.createObjectURL(blob) });\n\n const takePhoto = () => {\n const element = video.current;\n if (!element?.videoWidth) return;\n const canvas = document.createElement(\"canvas\");\n canvas.width = element.videoWidth;\n canvas.height = element.videoHeight;\n const context = canvas.getContext(\"2d\");\n if (!context) return;\n if (facing === \"user\") {\n context.translate(canvas.width, 0);\n context.scale(-1, 1);\n }\n context.drawImage(element, 0, 0);\n canvas.toBlob((blob) => blob && deliver(blob, \"photo\"), \"image/jpeg\", 0.92);\n };\n\n const toggleRecording = () => {\n if (recording) {\n recorder.current?.stop();\n return;\n }\n if (!stream.current) return;\n const chunks: Blob[] = [];\n const type = recorderType();\n const next = new MediaRecorder(stream.current, type ? { mimeType: type } : undefined);\n next.ondataavailable = (event) => event.data.size && chunks.push(event.data);\n next.onstop = () => {\n setRecording(false);\n setElapsed(0);\n deliver(new Blob(chunks, { type: next.mimeType || type || \"video/webm\" }), \"video\");\n };\n recorder.current = next;\n next.start(1000);\n setRecording(true);\n };\n\n const flip = () => {\n const side = facing === \"user\" ? \"environment\" : \"user\";\n setFacing(side);\n void start(side);\n };\n\n const accept = mode === \"video\" ? \"video/*\" : mode === \"photo\" ? \"image/*\" : \"image/*,video/*\";\n const fallback = (\n <>\n <button type=\"button\" className={s.secondary()} onClick={() => file.current?.click()}>\n {labels.fallback}\n </button>\n <input\n ref={file}\n type=\"file\"\n accept={accept}\n capture={facing}\n tabIndex={-1}\n aria-hidden\n className=\"sr-only\"\n onChange={(event) => {\n const picked = event.target.files?.[0];\n if (picked) deliver(picked, picked.type.startsWith(\"video/\") ? \"video\" : \"photo\");\n event.target.value = \"\";\n }}\n />\n </>\n );\n\n return (\n <section aria-label={labels.region} className={s.root({ className })}>\n <div className={s.stage()} style={{ aspectRatio }}>\n <video ref={video} muted playsInline aria-hidden data-mirrored={facing === \"user\" ? \"\" : undefined} className={s.video()} hidden={state !== \"live\"} />\n {state !== \"live\" && (\n <div className={s.panel()} role={state === \"denied\" || state === \"unavailable\" ? \"alert\" : undefined}>\n <span className={s.panelIcon()} aria-hidden>\n <CameraGlyph className=\"h-6 w-6\" />\n </span>\n <p className=\"max-w-xs\">{state === \"denied\" ? labels.denied : state === \"unavailable\" ? labels.unavailable : labels.prompt}</p>\n <div className=\"flex flex-wrap justify-center gap-2\">\n {(state === \"idle\" || state === \"starting\") && (\n <button type=\"button\" className={s.primary()} disabled={state === \"starting\"} onClick={() => void start(facing)}>\n <CameraGlyph className=\"h-4 w-4\" />\n {labels.start}\n </button>\n )}\n {fallback}\n </div>\n </div>\n )}\n {state === \"live\" && mode === \"both\" && !recording && (\n <div className={s.modes()}>\n <div className=\"flex gap-1 rounded-full bg-scrim-strong p-1\">\n <button type=\"button\" aria-pressed={kind === \"photo\"} className={s.mode()} onClick={() => setKind(\"photo\")}>\n {labels.photoMode}\n </button>\n <button type=\"button\" aria-pressed={kind === \"video\"} className={s.mode()} onClick={() => setKind(\"video\")}>\n {labels.videoMode}\n </button>\n </div>\n </div>\n )}\n {recording && (\n <span className={s.timer()} role=\"timer\" aria-live=\"off\">\n {formatClock(elapsed)}\n {maxDuration ? ` / ${formatClock(maxDuration)}` : \"\"}\n </span>\n )}\n {state === \"live\" && (\n <div className={s.bar()}>\n <span className=\"h-11 w-11\" />\n <button\n type=\"button\"\n className={s.shutter()}\n data-mode={kind}\n data-recording={recording ? \"\" : undefined}\n data-reduce={reduce ? \"\" : undefined}\n aria-label={kind === \"photo\" ? labels.takePhoto : recording ? labels.stopRecording : labels.startRecording}\n onClick={kind === \"photo\" ? takePhoto : toggleRecording}\n />\n <button type=\"button\" className={s.round()} aria-label={labels.switchCamera} disabled={cameras < 2 || recording} onClick={flip}>\n <FlipGlyph className=\"h-5 w-5\" />\n </button>\n </div>\n )}\n </div>\n </section>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
type MediaCaptureLabels = {
|
|
5
|
+
/** Why the camera is asked for, before the request ("Use your camera to take a photo"). */
|
|
6
|
+
prompt: string;
|
|
7
|
+
/** The button that asks ("Turn on the camera"). */
|
|
8
|
+
start: string;
|
|
9
|
+
/** Permission refused for good ("Camera access is off. Turn it on in Settings."). */
|
|
10
|
+
denied: string;
|
|
11
|
+
/** Opens the app's settings ("Open Settings"). */
|
|
12
|
+
settings: string;
|
|
13
|
+
takePhoto: string;
|
|
14
|
+
startRecording: string;
|
|
15
|
+
stopRecording: string;
|
|
16
|
+
switchCamera: string;
|
|
17
|
+
};
|
|
18
|
+
type NativeCaptureResult = {
|
|
19
|
+
/** The file of the photo or the clip, on the device. */
|
|
20
|
+
uri: string;
|
|
21
|
+
/** `photo` or `video`. */
|
|
22
|
+
kind: "photo" | "video";
|
|
23
|
+
};
|
|
24
|
+
type MediaCaptureProps = {
|
|
25
|
+
/** What it captures: `photo` (default) or `video`. */
|
|
26
|
+
mode?: "photo" | "video";
|
|
27
|
+
/** The camera at first: `front` (default) or `back`. */
|
|
28
|
+
facing?: "front" | "back";
|
|
29
|
+
/** The longest clip, in seconds. */
|
|
30
|
+
maxDuration?: number;
|
|
31
|
+
/** Called with the photo taken or the clip recorded. */
|
|
32
|
+
onCapture: (result: NativeCaptureResult) => void;
|
|
33
|
+
/** Called when a capture fails. */
|
|
34
|
+
onError?: (error: unknown) => void;
|
|
35
|
+
/** The width / height ratio of the preview. Default 3 / 4. */
|
|
36
|
+
aspectRatio?: number;
|
|
37
|
+
/** The words, passed translated. */
|
|
38
|
+
labels: MediaCaptureLabels;
|
|
39
|
+
/** Styles merged last, on the frame. */
|
|
40
|
+
style?: StyleProp<ViewStyle>;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The camera in a screen, on expo-camera: asks for the permission only when the person presses the button, sends them
|
|
44
|
+
* to Settings when it was refused for good, takes a photo or records a clip (and the microphone for a clip), switches
|
|
45
|
+
* cameras. The web `MediaCapture` with the same words.
|
|
46
|
+
*/
|
|
47
|
+
declare function MediaCapture({ mode, facing: initialFacing, maxDuration, onCapture, onError, aspectRatio, labels, style }: MediaCaptureProps): React.JSX.Element;
|
|
48
|
+
|
|
49
|
+
export { MediaCapture, type MediaCaptureLabels, type MediaCaptureProps, type NativeCaptureResult };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// src/native/media-capture.tsx
|
|
2
|
+
import { Button, Txt, useReducedMotion, useTheme } from "@krizaka/ui/native";
|
|
3
|
+
import { CameraView, useCameraPermissions, useMicrophonePermissions } from "expo-camera";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { Linking, Pressable, StyleSheet, View } from "react-native";
|
|
6
|
+
import Svg, { Path } from "react-native-svg";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
function MediaCapture({ mode = "photo", facing: initialFacing = "front", maxDuration, onCapture, onError, aspectRatio = 3 / 4, labels, style }) {
|
|
9
|
+
const { theme, radius } = useTheme();
|
|
10
|
+
const reduce = useReducedMotion();
|
|
11
|
+
const camera = React.useRef(null);
|
|
12
|
+
const [permission, requestPermission] = useCameraPermissions();
|
|
13
|
+
const [microphone, requestMicrophone] = useMicrophonePermissions();
|
|
14
|
+
const [facing, setFacing] = React.useState(initialFacing);
|
|
15
|
+
const [ready, setReady] = React.useState(false);
|
|
16
|
+
const [recording, setRecording] = React.useState(false);
|
|
17
|
+
const ask = async () => {
|
|
18
|
+
const result = await requestPermission();
|
|
19
|
+
if (result.granted && mode === "video" && !microphone?.granted) await requestMicrophone();
|
|
20
|
+
};
|
|
21
|
+
const shoot = async () => {
|
|
22
|
+
if (!camera.current || !ready) return;
|
|
23
|
+
try {
|
|
24
|
+
if (mode === "photo") {
|
|
25
|
+
const photo = await camera.current.takePictureAsync({ quality: 0.9 });
|
|
26
|
+
if (photo) onCapture({ uri: photo.uri, kind: "photo" });
|
|
27
|
+
} else if (recording) {
|
|
28
|
+
camera.current.stopRecording();
|
|
29
|
+
} else {
|
|
30
|
+
setRecording(true);
|
|
31
|
+
const clip = await camera.current.recordAsync(maxDuration ? { maxDuration } : void 0);
|
|
32
|
+
setRecording(false);
|
|
33
|
+
if (clip) onCapture({ uri: clip.uri, kind: "video" });
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
setRecording(false);
|
|
37
|
+
onError?.(error);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const frame = [styles.frame, { aspectRatio, borderRadius: radius.md, backgroundColor: theme.media }, style];
|
|
41
|
+
if (!permission?.granted) {
|
|
42
|
+
const blocked = permission ? !permission.canAskAgain : false;
|
|
43
|
+
return /* @__PURE__ */ jsxs(View, { style: [frame, styles.panel], children: [
|
|
44
|
+
/* @__PURE__ */ jsx(Txt, { style: styles.center, children: blocked ? labels.denied : labels.prompt }),
|
|
45
|
+
blocked ? /* @__PURE__ */ jsx(Button, { label: labels.settings, variant: "secondary", onPress: () => void Linking.openSettings() }) : /* @__PURE__ */ jsx(Button, { label: labels.start, variant: "primary", onPress: () => void ask() })
|
|
46
|
+
] });
|
|
47
|
+
}
|
|
48
|
+
return /* @__PURE__ */ jsxs(View, { style: frame, children: [
|
|
49
|
+
/* @__PURE__ */ jsx(
|
|
50
|
+
CameraView,
|
|
51
|
+
{
|
|
52
|
+
ref: camera,
|
|
53
|
+
style: StyleSheet.absoluteFill,
|
|
54
|
+
facing,
|
|
55
|
+
mode: mode === "video" ? "video" : "picture",
|
|
56
|
+
mirror: facing === "front",
|
|
57
|
+
onCameraReady: () => setReady(true),
|
|
58
|
+
"aria-hidden": true
|
|
59
|
+
}
|
|
60
|
+
),
|
|
61
|
+
/* @__PURE__ */ jsxs(View, { style: styles.bar, children: [
|
|
62
|
+
/* @__PURE__ */ jsx(View, { style: styles.side }),
|
|
63
|
+
/* @__PURE__ */ jsx(
|
|
64
|
+
Pressable,
|
|
65
|
+
{
|
|
66
|
+
role: "button",
|
|
67
|
+
"aria-label": mode === "photo" ? labels.takePhoto : recording ? labels.stopRecording : labels.startRecording,
|
|
68
|
+
"aria-disabled": !ready,
|
|
69
|
+
disabled: !ready,
|
|
70
|
+
onPress: () => void shoot(),
|
|
71
|
+
style: ({ pressed }) => [styles.shutter, { borderColor: theme.textOnMedia, opacity: pressed && !reduce ? 0.8 : 1 }],
|
|
72
|
+
children: /* @__PURE__ */ jsx(
|
|
73
|
+
View,
|
|
74
|
+
{
|
|
75
|
+
style: [
|
|
76
|
+
mode === "video" && recording ? styles.stop : styles.dot,
|
|
77
|
+
{ backgroundColor: mode === "video" ? theme.danger : theme.textOnMedia }
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
),
|
|
83
|
+
/* @__PURE__ */ jsx(
|
|
84
|
+
Pressable,
|
|
85
|
+
{
|
|
86
|
+
role: "button",
|
|
87
|
+
"aria-label": labels.switchCamera,
|
|
88
|
+
"aria-disabled": recording,
|
|
89
|
+
disabled: recording,
|
|
90
|
+
hitSlop: 8,
|
|
91
|
+
onPress: () => setFacing((f) => f === "front" ? "back" : "front"),
|
|
92
|
+
style: [styles.side, styles.flip, { backgroundColor: theme.scrimStrong, opacity: recording ? 0 : 1 }],
|
|
93
|
+
children: /* @__PURE__ */ jsx(Svg, { width: 22, height: 22, viewBox: "0 0 24 24", fill: "none", stroke: theme.textOnMedia, strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ jsx(Path, { d: "M4 12a8 8 0 0 1 14-5.3M20 12a8 8 0 0 1-14 5.3M18 3v4h-4M6 21v-4h4" }) })
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
] })
|
|
97
|
+
] });
|
|
98
|
+
}
|
|
99
|
+
var styles = StyleSheet.create({
|
|
100
|
+
frame: { width: "100%", overflow: "hidden" },
|
|
101
|
+
panel: { alignItems: "center", justifyContent: "center", gap: 16, padding: 24 },
|
|
102
|
+
center: { textAlign: "center" },
|
|
103
|
+
bar: { position: "absolute", left: 0, right: 0, bottom: 16, flexDirection: "row", alignItems: "center", justifyContent: "space-evenly" },
|
|
104
|
+
side: { width: 48, height: 48 },
|
|
105
|
+
flip: { borderRadius: 24, alignItems: "center", justifyContent: "center" },
|
|
106
|
+
shutter: { width: 72, height: 72, borderRadius: 36, borderWidth: 4, alignItems: "center", justifyContent: "center" },
|
|
107
|
+
dot: { width: 54, height: 54, borderRadius: 27 },
|
|
108
|
+
stop: { width: 26, height: 26, borderRadius: 6 }
|
|
109
|
+
});
|
|
110
|
+
export {
|
|
111
|
+
MediaCapture
|
|
112
|
+
};
|
|
113
|
+
//# sourceMappingURL=media-capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/native/media-capture.tsx"],"sourcesContent":["// @krizaka/ui/native/media-capture — its own entry, because it imports expo-camera: an optional peer, installed only\n// by the apps that use the camera. The theme and the buttons come from @krizaka/ui/native.\nimport { Button, Txt, useReducedMotion, useTheme } from \"@krizaka/ui/native\";\nimport { CameraView, useCameraPermissions, useMicrophonePermissions } from \"expo-camera\";\nimport * as React from \"react\";\nimport { Linking, Pressable, type StyleProp, StyleSheet, View, type ViewStyle } from \"react-native\";\nimport Svg, { Path } from \"react-native-svg\";\n\nexport type MediaCaptureLabels = {\n /** Why the camera is asked for, before the request (\"Use your camera to take a photo\"). */\n prompt: string;\n /** The button that asks (\"Turn on the camera\"). */\n start: string;\n /** Permission refused for good (\"Camera access is off. Turn it on in Settings.\"). */\n denied: string;\n /** Opens the app's settings (\"Open Settings\"). */\n settings: string;\n takePhoto: string;\n startRecording: string;\n stopRecording: string;\n switchCamera: string;\n};\n\nexport type NativeCaptureResult = {\n /** The file of the photo or the clip, on the device. */\n uri: string;\n /** `photo` or `video`. */\n kind: \"photo\" | \"video\";\n};\n\nexport type MediaCaptureProps = {\n /** What it captures: `photo` (default) or `video`. */\n mode?: \"photo\" | \"video\";\n /** The camera at first: `front` (default) or `back`. */\n facing?: \"front\" | \"back\";\n /** The longest clip, in seconds. */\n maxDuration?: number;\n /** Called with the photo taken or the clip recorded. */\n onCapture: (result: NativeCaptureResult) => void;\n /** Called when a capture fails. */\n onError?: (error: unknown) => void;\n /** The width / height ratio of the preview. Default 3 / 4. */\n aspectRatio?: number;\n /** The words, passed translated. */\n labels: MediaCaptureLabels;\n /** Styles merged last, on the frame. */\n style?: StyleProp<ViewStyle>;\n};\n\n/**\n * The camera in a screen, on expo-camera: asks for the permission only when the person presses the button, sends them\n * to Settings when it was refused for good, takes a photo or records a clip (and the microphone for a clip), switches\n * cameras. The web `MediaCapture` with the same words.\n */\nexport function MediaCapture({ mode = \"photo\", facing: initialFacing = \"front\", maxDuration, onCapture, onError, aspectRatio = 3 / 4, labels, style }: MediaCaptureProps) {\n const { theme, radius } = useTheme();\n const reduce = useReducedMotion();\n const camera = React.useRef<CameraView>(null);\n const [permission, requestPermission] = useCameraPermissions();\n const [microphone, requestMicrophone] = useMicrophonePermissions();\n const [facing, setFacing] = React.useState(initialFacing);\n const [ready, setReady] = React.useState(false);\n const [recording, setRecording] = React.useState(false);\n\n const ask = async () => {\n const result = await requestPermission();\n if (result.granted && mode === \"video\" && !microphone?.granted) await requestMicrophone();\n };\n\n const shoot = async () => {\n if (!camera.current || !ready) return;\n try {\n if (mode === \"photo\") {\n const photo = await camera.current.takePictureAsync({ quality: 0.9 });\n if (photo) onCapture({ uri: photo.uri, kind: \"photo\" });\n } else if (recording) {\n camera.current.stopRecording();\n } else {\n setRecording(true);\n const clip = await camera.current.recordAsync(maxDuration ? { maxDuration } : undefined);\n setRecording(false);\n if (clip) onCapture({ uri: clip.uri, kind: \"video\" });\n }\n } catch (error) {\n setRecording(false);\n onError?.(error);\n }\n };\n\n const frame = [styles.frame, { aspectRatio, borderRadius: radius.md, backgroundColor: theme.media }, style];\n\n if (!permission?.granted) {\n const blocked = permission ? !permission.canAskAgain : false;\n return (\n <View style={[frame, styles.panel]}>\n <Txt style={styles.center}>\n {blocked ? labels.denied : labels.prompt}\n </Txt>\n {blocked ? (\n <Button label={labels.settings} variant=\"secondary\" onPress={() => void Linking.openSettings()} />\n ) : (\n <Button label={labels.start} variant=\"primary\" onPress={() => void ask()} />\n )}\n </View>\n );\n }\n\n return (\n <View style={frame}>\n <CameraView\n ref={camera}\n style={StyleSheet.absoluteFill}\n facing={facing}\n mode={mode === \"video\" ? \"video\" : \"picture\"}\n mirror={facing === \"front\"}\n onCameraReady={() => setReady(true)}\n aria-hidden\n />\n <View style={styles.bar}>\n <View style={styles.side} />\n <Pressable\n role=\"button\"\n aria-label={mode === \"photo\" ? labels.takePhoto : recording ? labels.stopRecording : labels.startRecording}\n aria-disabled={!ready}\n disabled={!ready}\n onPress={() => void shoot()}\n style={({ pressed }) => [styles.shutter, { borderColor: theme.textOnMedia, opacity: pressed && !reduce ? 0.8 : 1 }]}\n >\n <View\n style={[\n mode === \"video\" && recording ? styles.stop : styles.dot,\n { backgroundColor: mode === \"video\" ? theme.danger : theme.textOnMedia },\n ]}\n />\n </Pressable>\n <Pressable\n role=\"button\"\n aria-label={labels.switchCamera}\n aria-disabled={recording}\n disabled={recording}\n hitSlop={8}\n onPress={() => setFacing((f) => (f === \"front\" ? \"back\" : \"front\"))}\n style={[styles.side, styles.flip, { backgroundColor: theme.scrimStrong, opacity: recording ? 0 : 1 }]}\n >\n <Svg width={22} height={22} viewBox=\"0 0 24 24\" fill=\"none\" stroke={theme.textOnMedia} strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <Path d=\"M4 12a8 8 0 0 1 14-5.3M20 12a8 8 0 0 1-14 5.3M18 3v4h-4M6 21v-4h4\" />\n </Svg>\n </Pressable>\n </View>\n </View>\n );\n}\n\nconst styles = StyleSheet.create({\n frame: { width: \"100%\", overflow: \"hidden\" },\n panel: { alignItems: \"center\", justifyContent: \"center\", gap: 16, padding: 24 },\n center: { textAlign: \"center\" },\n bar: { position: \"absolute\", left: 0, right: 0, bottom: 16, flexDirection: \"row\", alignItems: \"center\", justifyContent: \"space-evenly\" },\n side: { width: 48, height: 48 },\n flip: { borderRadius: 24, alignItems: \"center\", justifyContent: \"center\" },\n shutter: { width: 72, height: 72, borderRadius: 36, borderWidth: 4, alignItems: \"center\", justifyContent: \"center\" },\n dot: { width: 54, height: 54, borderRadius: 27 },\n stop: { width: 26, height: 26, borderRadius: 6 },\n});\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ContentType, VideoContentFit } from 'expo-video';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { ImageSourcePropType, StyleProp, ViewStyle } from 'react-native';
|
|
4
|
+
|
|
5
|
+
type VideoPlayerSource = {
|
|
6
|
+
/** The address (MP4 or an HLS `.m3u8`, signed by your server). */
|
|
7
|
+
uri: string;
|
|
8
|
+
/** `hls` when the address does not end in `.m3u8`; `auto` (default) otherwise. */
|
|
9
|
+
contentType?: ContentType;
|
|
10
|
+
/** Headers sent with the request (an authorization). */
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
};
|
|
13
|
+
type VideoPlayerProps = {
|
|
14
|
+
/** The video. A new source replaces the current one. */
|
|
15
|
+
source: VideoPlayerSource;
|
|
16
|
+
/** The accessible name of the player ("Video: Sunset over Montréal"), passed translated. */
|
|
17
|
+
title: string;
|
|
18
|
+
/** The picture shown until the first frame. */
|
|
19
|
+
poster?: ImageSourcePropType;
|
|
20
|
+
/** Starts playing (muted unless `muted={false}`) — not when the system asks to reduce motion. */
|
|
21
|
+
autoPlay?: boolean;
|
|
22
|
+
/** Starts muted. Default: `autoPlay`. */
|
|
23
|
+
muted?: boolean;
|
|
24
|
+
/** Plays again from the start at the end. */
|
|
25
|
+
loop?: boolean;
|
|
26
|
+
/** Pauses while `true`: the app passes it when the video leaves the screen (a FlatList's viewability). */
|
|
27
|
+
paused?: boolean;
|
|
28
|
+
/** The platform's controls (default `true`: accessible, with full screen). `false` for a story or a background. */
|
|
29
|
+
nativeControls?: boolean;
|
|
30
|
+
/** Allows full screen from the controls. Default `true`. */
|
|
31
|
+
fullscreen?: boolean;
|
|
32
|
+
/** Allows picture in picture. Default `false`. */
|
|
33
|
+
pictureInPicture?: boolean;
|
|
34
|
+
/** How the picture fills the frame: `contain` (default), `cover`, `fill`. */
|
|
35
|
+
contentFit?: VideoContentFit;
|
|
36
|
+
/** The width / height ratio of the frame. Default 16 / 9. */
|
|
37
|
+
aspectRatio?: number;
|
|
38
|
+
/** Called when it starts or stops playing. */
|
|
39
|
+
onPlayingChange?: (playing: boolean) => void;
|
|
40
|
+
/** Styles merged last, on the frame. */
|
|
41
|
+
style?: StyleProp<ViewStyle>;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* A video on expo-video: the platform's own player and controls (accessible, full screen, picture in picture), the
|
|
45
|
+
* poster until the first frame, autoplay that waits under reduced motion, and `paused` to stop it off screen. The
|
|
46
|
+
* web `VideoPlayer` draws its own controls; here the system's are the right ones.
|
|
47
|
+
*/
|
|
48
|
+
declare function VideoPlayer({ source, title, poster, autoPlay, muted, loop, paused, nativeControls, fullscreen, pictureInPicture, contentFit, aspectRatio, onPlayingChange, style, }: VideoPlayerProps): React.JSX.Element;
|
|
49
|
+
|
|
50
|
+
export { VideoPlayer, type VideoPlayerProps, type VideoPlayerSource };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/native/video-player.tsx
|
|
2
|
+
import { useTheme } from "@krizaka/ui/native";
|
|
3
|
+
import { useVideoPlayer, VideoView } from "expo-video";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { AccessibilityInfo, Image, StyleSheet, View } from "react-native";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
function configure(player, loop, muted) {
|
|
8
|
+
player.loop = loop;
|
|
9
|
+
player.muted = muted;
|
|
10
|
+
}
|
|
11
|
+
function VideoPlayer({
|
|
12
|
+
source,
|
|
13
|
+
title,
|
|
14
|
+
poster,
|
|
15
|
+
autoPlay = false,
|
|
16
|
+
muted = autoPlay,
|
|
17
|
+
loop = false,
|
|
18
|
+
paused = false,
|
|
19
|
+
nativeControls = true,
|
|
20
|
+
fullscreen = true,
|
|
21
|
+
pictureInPicture = false,
|
|
22
|
+
contentFit = "contain",
|
|
23
|
+
aspectRatio = 16 / 9,
|
|
24
|
+
onPlayingChange,
|
|
25
|
+
style
|
|
26
|
+
}) {
|
|
27
|
+
const { theme, radius } = useTheme();
|
|
28
|
+
const [firstFrame, setFirstFrame] = React.useState(false);
|
|
29
|
+
const player = useVideoPlayer({ uri: source.uri, contentType: source.contentType, headers: source.headers }, (p) => configure(p, loop, muted));
|
|
30
|
+
React.useEffect(() => configure(player, loop, muted), [player, loop, muted]);
|
|
31
|
+
React.useEffect(() => {
|
|
32
|
+
let alive = true;
|
|
33
|
+
if (paused) player.pause();
|
|
34
|
+
else if (autoPlay)
|
|
35
|
+
void AccessibilityInfo.isReduceMotionEnabled().then((reduce) => {
|
|
36
|
+
if (alive && !reduce) player.play();
|
|
37
|
+
});
|
|
38
|
+
return () => {
|
|
39
|
+
alive = false;
|
|
40
|
+
};
|
|
41
|
+
}, [player, paused, autoPlay]);
|
|
42
|
+
React.useEffect(() => {
|
|
43
|
+
if (!onPlayingChange) return;
|
|
44
|
+
const subscription = player.addListener("playingChange", ({ isPlaying }) => onPlayingChange(isPlaying));
|
|
45
|
+
return () => subscription.remove();
|
|
46
|
+
}, [player, onPlayingChange]);
|
|
47
|
+
return /* @__PURE__ */ jsxs(View, { role: "group", "aria-label": title, style: [styles.frame, { aspectRatio, borderRadius: radius.sm, backgroundColor: theme.media }, style], children: [
|
|
48
|
+
/* @__PURE__ */ jsx(
|
|
49
|
+
VideoView,
|
|
50
|
+
{
|
|
51
|
+
player,
|
|
52
|
+
style: StyleSheet.absoluteFill,
|
|
53
|
+
contentFit,
|
|
54
|
+
nativeControls,
|
|
55
|
+
fullscreenOptions: { enable: fullscreen },
|
|
56
|
+
allowsPictureInPicture: pictureInPicture,
|
|
57
|
+
onFirstFrameRender: () => setFirstFrame(true)
|
|
58
|
+
}
|
|
59
|
+
),
|
|
60
|
+
poster && !firstFrame ? /* @__PURE__ */ jsx(View, { pointerEvents: "none", style: StyleSheet.absoluteFill, children: /* @__PURE__ */ jsx(Image, { source: poster, "aria-hidden": true, resizeMode: "cover", style: StyleSheet.absoluteFill }) }) : null
|
|
61
|
+
] });
|
|
62
|
+
}
|
|
63
|
+
var styles = StyleSheet.create({
|
|
64
|
+
frame: { width: "100%", overflow: "hidden" }
|
|
65
|
+
});
|
|
66
|
+
export {
|
|
67
|
+
VideoPlayer
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=video-player.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/native/video-player.tsx"],"sourcesContent":["// @krizaka/ui/native/video-player — its own entry, because it imports expo-video: an optional peer, installed only by\n// the apps that play video. The theme comes from @krizaka/ui/native (the same context as the rest of the platform).\nimport { useTheme } from \"@krizaka/ui/native\";\nimport { type ContentType, useVideoPlayer, type VideoContentFit, VideoView } from \"expo-video\";\nimport * as React from \"react\";\nimport { AccessibilityInfo, Image, type ImageSourcePropType, type StyleProp, StyleSheet, View, type ViewStyle } from \"react-native\";\n\nexport type VideoPlayerSource = {\n /** The address (MP4 or an HLS `.m3u8`, signed by your server). */\n uri: string;\n /** `hls` when the address does not end in `.m3u8`; `auto` (default) otherwise. */\n contentType?: ContentType;\n /** Headers sent with the request (an authorization). */\n headers?: Record<string, string>;\n};\n\nexport type VideoPlayerProps = {\n /** The video. A new source replaces the current one. */\n source: VideoPlayerSource;\n /** The accessible name of the player (\"Video: Sunset over Montréal\"), passed translated. */\n title: string;\n /** The picture shown until the first frame. */\n poster?: ImageSourcePropType;\n /** Starts playing (muted unless `muted={false}`) — not when the system asks to reduce motion. */\n autoPlay?: boolean;\n /** Starts muted. Default: `autoPlay`. */\n muted?: boolean;\n /** Plays again from the start at the end. */\n loop?: boolean;\n /** Pauses while `true`: the app passes it when the video leaves the screen (a FlatList's viewability). */\n paused?: boolean;\n /** The platform's controls (default `true`: accessible, with full screen). `false` for a story or a background. */\n nativeControls?: boolean;\n /** Allows full screen from the controls. Default `true`. */\n fullscreen?: boolean;\n /** Allows picture in picture. Default `false`. */\n pictureInPicture?: boolean;\n /** How the picture fills the frame: `contain` (default), `cover`, `fill`. */\n contentFit?: VideoContentFit;\n /** The width / height ratio of the frame. Default 16 / 9. */\n aspectRatio?: number;\n /** Called when it starts or stops playing. */\n onPlayingChange?: (playing: boolean) => void;\n /** Styles merged last, on the frame. */\n style?: StyleProp<ViewStyle>;\n};\n\n/** The event surface of expo-video's player (a SharedObject of `expo`), typed here so `expo` stays the app's. */\ntype Emitter = { addListener(event: \"playingChange\", listener: (payload: { isPlaying: boolean }) => void): { remove(): void } };\n\n/** Applies the options a player can change while it lives (outside the component: the player is the hook's). */\nfunction configure(player: { loop: boolean; muted: boolean }, loop: boolean, muted: boolean) {\n player.loop = loop;\n player.muted = muted;\n}\n\n/**\n * A video on expo-video: the platform's own player and controls (accessible, full screen, picture in picture), the\n * poster until the first frame, autoplay that waits under reduced motion, and `paused` to stop it off screen. The\n * web `VideoPlayer` draws its own controls; here the system's are the right ones.\n */\nexport function VideoPlayer({\n source,\n title,\n poster,\n autoPlay = false,\n muted = autoPlay,\n loop = false,\n paused = false,\n nativeControls = true,\n fullscreen = true,\n pictureInPicture = false,\n contentFit = \"contain\",\n aspectRatio = 16 / 9,\n onPlayingChange,\n style,\n}: VideoPlayerProps) {\n const { theme, radius } = useTheme();\n const [firstFrame, setFirstFrame] = React.useState(false);\n const player = useVideoPlayer({ uri: source.uri, contentType: source.contentType, headers: source.headers }, (p) => configure(p, loop, muted));\n\n React.useEffect(() => configure(player, loop, muted), [player, loop, muted]);\n\n React.useEffect(() => {\n let alive = true;\n if (paused) player.pause();\n else if (autoPlay)\n void AccessibilityInfo.isReduceMotionEnabled().then((reduce) => {\n if (alive && !reduce) player.play();\n });\n return () => {\n alive = false;\n };\n }, [player, paused, autoPlay]);\n\n React.useEffect(() => {\n if (!onPlayingChange) return;\n const subscription = (player as unknown as Emitter).addListener(\"playingChange\", ({ isPlaying }) => onPlayingChange(isPlaying));\n return () => subscription.remove();\n }, [player, onPlayingChange]);\n\n return (\n <View role=\"group\" aria-label={title} style={[styles.frame, { aspectRatio, borderRadius: radius.sm, backgroundColor: theme.media }, style]}>\n <VideoView\n player={player}\n style={StyleSheet.absoluteFill}\n contentFit={contentFit}\n nativeControls={nativeControls}\n fullscreenOptions={{ enable: fullscreen }}\n allowsPictureInPicture={pictureInPicture}\n onFirstFrameRender={() => setFirstFrame(true)}\n />\n {poster && !firstFrame ? (\n <View pointerEvents=\"none\" style={StyleSheet.absoluteFill}>\n <Image source={poster} aria-hidden resizeMode=\"cover\" style={StyleSheet.absoluteFill} />\n </View>\n ) : null}\n </View>\n );\n}\n\nconst styles = StyleSheet.create({\n frame: { width: \"100%\", overflow: \"hidden\" },\n});\n"],"mappings":";AAEA,SAAS,gBAAgB;AACzB,SAA2B,gBAAsC,iBAAiB;AAClF,YAAY,WAAW;AACvB,SAAS,mBAAmB,OAAiD,YAAY,YAA4B;AAiGjH,SACE,KADF;AAnDJ,SAAS,UAAU,QAA2C,MAAe,OAAgB;AAC3F,SAAO,OAAO;AACd,SAAO,QAAQ;AACjB;AAOO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,SAAS;AAAA,EACT,iBAAiB;AAAA,EACjB,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,aAAa;AAAA,EACb,cAAc,KAAK;AAAA,EACnB;AAAA,EACA;AACF,GAAqB;AACnB,QAAM,EAAE,OAAO,OAAO,IAAI,SAAS;AACnC,QAAM,CAAC,YAAY,aAAa,IAAU,eAAS,KAAK;AACxD,QAAM,SAAS,eAAe,EAAE,KAAK,OAAO,KAAK,aAAa,OAAO,aAAa,SAAS,OAAO,QAAQ,GAAG,CAAC,MAAM,UAAU,GAAG,MAAM,KAAK,CAAC;AAE7I,EAAM,gBAAU,MAAM,UAAU,QAAQ,MAAM,KAAK,GAAG,CAAC,QAAQ,MAAM,KAAK,CAAC;AAE3E,EAAM,gBAAU,MAAM;AACpB,QAAI,QAAQ;AACZ,QAAI,OAAQ,QAAO,MAAM;AAAA,aAChB;AACP,WAAK,kBAAkB,sBAAsB,EAAE,KAAK,CAAC,WAAW;AAC9D,YAAI,SAAS,CAAC,OAAQ,QAAO,KAAK;AAAA,MACpC,CAAC;AACH,WAAO,MAAM;AACX,cAAQ;AAAA,IACV;AAAA,EACF,GAAG,CAAC,QAAQ,QAAQ,QAAQ,CAAC;AAE7B,EAAM,gBAAU,MAAM;AACpB,QAAI,CAAC,gBAAiB;AACtB,UAAM,eAAgB,OAA8B,YAAY,iBAAiB,CAAC,EAAE,UAAU,MAAM,gBAAgB,SAAS,CAAC;AAC9H,WAAO,MAAM,aAAa,OAAO;AAAA,EACnC,GAAG,CAAC,QAAQ,eAAe,CAAC;AAE5B,SACE,qBAAC,QAAK,MAAK,SAAQ,cAAY,OAAO,OAAO,CAAC,OAAO,OAAO,EAAE,aAAa,cAAc,OAAO,IAAI,iBAAiB,MAAM,MAAM,GAAG,KAAK,GACvI;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,OAAO,WAAW;AAAA,QAClB;AAAA,QACA;AAAA,QACA,mBAAmB,EAAE,QAAQ,WAAW;AAAA,QACxC,wBAAwB;AAAA,QACxB,oBAAoB,MAAM,cAAc,IAAI;AAAA;AAAA,IAC9C;AAAA,IACC,UAAU,CAAC,aACV,oBAAC,QAAK,eAAc,QAAO,OAAO,WAAW,cAC3C,8BAAC,SAAM,QAAQ,QAAQ,eAAW,MAAC,YAAW,SAAQ,OAAO,WAAW,cAAc,GACxF,IACE;AAAA,KACN;AAEJ;AAEA,IAAM,SAAS,WAAW,OAAO;AAAA,EAC/B,OAAO,EAAE,OAAO,QAAQ,UAAU,SAAS;AAC7C,CAAC;","names":[]}
|