UNPKG

use-simple-camera

Version:

Production-ready React Hooks for Camera, Video Recording, QR/Barcode Scanning, Motion Detection, and Audio Analysis. Zero dependencies, fully typed, and easy to use.

2 lines (1 loc) 14.5 kB
(function(M,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("react")):typeof define=="function"&&define.amd?define(["exports","react"],e):(M=typeof globalThis<"u"?globalThis:M||self,e(M.UseSimpleCamera={},M.React))})(this,function(M,e){"use strict";const K=c=>{const[r,i]=e.useState(0),k=e.useRef(null),d=e.useRef(null),w=e.useRef(null);return e.useEffect(()=>{if(!c){i(0);return}if(!c.getAudioTracks()[0])return;const u=window.AudioContext||window.webkitAudioContext;if(!u){console.warn("AudioContext not supported");return}const y=new u;k.current=y;const h=y.createMediaStreamSource(c),E=y.createAnalyser();E.fftSize=256,h.connect(E),d.current=E;const v=new Uint8Array(E.frequencyBinCount),b=()=>{if(!d.current)return;d.current.getByteFrequencyData(v);let f=0;for(let s=0;s<v.length;s++)f+=v[s];const l=f/v.length,o=Math.min(100,Math.round(l/255*100*2.5));i(o),w.current=requestAnimationFrame(b)};return b(),()=>{w.current&&cancelAnimationFrame(w.current),k.current&&k.current.close().catch(console.error)}},[c]),{volume:r}},Z=(c,r)=>{const{onDetect:i,formats:k=["qr_code"]}=r,[d,w]=e.useState(!1),[t,u]=e.useState(!1),y=e.useRef(null);return e.useEffect(()=>{window.BarcodeDetector?window.BarcodeDetector.getSupportedFormats().then(h=>{const E=k.some(v=>h.includes(v));w(E)}).catch(()=>w(!1)):w(!1)},[k]),e.useEffect(()=>{if(!c||!d)return;const h=document.createElement("video");h.srcObject=c,h.muted=!0,h.play().catch(console.error);const E=new window.BarcodeDetector({formats:k}),v=async()=>{if(h.videoWidth!==0)try{const b=await E.detect(h);b.length>0&&b.forEach(f=>i(f.rawValue))}catch(b){console.warn("Barcode detection failed",b)}};return u(!0),y.current=setInterval(v,500),()=>{y.current&&clearInterval(y.current),h.pause(),h.srcObject=null}},[c,d]),{isSupported:d,isScanning:t}},$=c=>{var L,V;const[r,i]=e.useState(null),[k,d]=e.useState(null),[w,t]=e.useState(1),[u,y]=e.useState(!1),[h,E]=e.useState(0),[v,b]=e.useState(0),[f,l]=e.useState("none"),[o,s]=e.useState(0);e.useEffect(()=>{var z,F;if(!c){i(null),d(null);return}const g=c.getVideoTracks()[0];if(!g)return;const P=((z=g.getCapabilities)==null?void 0:z.call(g))||{},A=((F=g.getSettings)==null?void 0:F.call(g))||{};i(P),d(A),A.zoom&&t(A.zoom);const I=A;I.pan&&E(I.pan),I.tilt&&b(I.tilt),I.focusMode&&l(I.focusMode),I.focusDistance&&s(I.focusDistance)},[c]);const a=e.useCallback(async g=>{if(!c)return;const P=c.getVideoTracks()[0];if(P)try{await P.applyConstraints({advanced:[g]});const A=P.getSettings();d(A)}catch(A){console.error("Failed to apply constraints:",A)}},[c]),p=e.useCallback(async g=>{await a({zoom:g}),t(g)},[a]),m=e.useCallback(async g=>{await a({torch:g}),y(g)},[a]),R=e.useCallback(async g=>{await a({pan:g}),E(g)},[a]),C=e.useCallback(async g=>{await a({tilt:g}),b(g)},[a]),N=e.useCallback(async g=>{await a({focusMode:g}),l(g)},[a]),O=e.useCallback(async g=>{await a({focusDistance:g}),s(g)},[a]);return{zoom:w,minZoom:((L=r==null?void 0:r.zoom)==null?void 0:L.min)||1,maxZoom:((V=r==null?void 0:r.zoom)==null?void 0:V.max)||1,flash:u,hasFlash:!!(r!=null&&r.torch),pan:h,tilt:v,focusMode:f,focusDistance:o,setZoom:p,setFlash:m,setPan:R,setTilt:C,setFocusMode:N,setFocusDistance:O,supports:{zoom:!!(r!=null&&r.zoom),flash:!!(r!=null&&r.torch),pan:!!(r!=null&&r.pan),tilt:!!(r!=null&&r.tilt),focusMode:!!(r!=null&&r.focusMode),focusDistance:!!(r!=null&&r.focusDistance)}}},G=()=>{const[c,r]=e.useState([]),[i,k]=e.useState([]),[d,w]=e.useState([]),t=e.useCallback(async()=>{try{const u=await navigator.mediaDevices.enumerateDevices();r(u),k(u.filter(y=>y.kind==="videoinput")),w(u.filter(y=>y.kind==="audioinput"))}catch(u){console.error("Error enumerating devices:",u)}},[]);return e.useEffect(()=>(t(),navigator.mediaDevices.addEventListener("devicechange",t),()=>{navigator.mediaDevices.removeEventListener("devicechange",t)}),[t]),{devices:c,videoDevices:i,audioDevices:d,enumerateDevices:t}},X=(c,r={})=>{const{sensitivity:i=.2,intervalMs:k=100,onMotion:d}=r,[w,t]=e.useState(!1),u=e.useRef(null),y=e.useRef(null),h=e.useRef(null);return e.useEffect(()=>{if(!c||!c.getVideoTracks()[0])return;const v=document.createElement("video");v.srcObject=c,v.muted=!0,v.play().catch(console.error);const b=document.createElement("canvas");b.width=100,b.height=100;const f=b.getContext("2d",{willReadFrequently:!0});y.current=b;const l=()=>{if(!f||v.videoWidth===0)return;f.drawImage(v,0,0,100,100);const o=f.getImageData(0,0,100,100).data;if(h.current){let s=0;for(let R=0;R<o.length;R+=4){const C=Math.abs(o[R]-h.current[R]),N=Math.abs(o[R+1]-h.current[R+1]),O=Math.abs(o[R+2]-h.current[R+2]);C+N+O>100&&s++}const a=100*100;s/a>i?(t(!0),d==null||d()):t(!1)}h.current=o};return u.current=setInterval(l,k),()=>{u.current&&clearInterval(u.current),v.pause(),v.srcObject=null}},[c,i,k]),{motionDetected:w}},J=()=>{const[c,r]=e.useState("portrait"),[i,k]=e.useState(0);return e.useEffect(()=>{var w;const d=()=>{var y;if(!((y=window.screen)!=null&&y.orientation))return;const t=window.screen.orientation.type,u=window.screen.orientation.angle;k(u),t.includes("portrait")?r("portrait"):r("landscape")};if(d(),(w=window.screen)!=null&&w.orientation)return window.screen.orientation.addEventListener("change",d),()=>{window.screen.orientation.removeEventListener("change",d)}},[]),{orientation:c,angle:i}},Q=c=>{const[r,i]=e.useState(!1),[k,d]=e.useState(!1),[w,t]=e.useState([]),u=e.useRef(null),y=e.useRef(null),h=e.useCallback(o=>{if(!c)throw new Error("No stream available to record");if(!r)try{let s=c;const a=(o==null?void 0:o.mode)||"both";if(a==="video-only"){const C=c.getVideoTracks();C.length>0&&(s=new MediaStream(C))}else if(a==="audio-only"){const C=c.getAudioTracks();C.length>0&&(s=new MediaStream(C))}const p=(o==null?void 0:o.mimeType)||(a==="audio-only"?"audio/webm":"video/webm"),m=new MediaRecorder(s,{mimeType:p});u.current=m;const R=[];m.ondataavailable=C=>{C.data.size>0&&R.push(C.data)},m.onstop=()=>{const C=new Blob(R,{type:p});i(!1),d(!1),t(R),y.current&&clearTimeout(y.current),o!=null&&o.onComplete&&o.onComplete(C)},m.start(1e3),i(!0),d(!1),o!=null&&o.timeLimitMs&&(y.current=setTimeout(()=>{E()},o.timeLimitMs))}catch(s){console.error("Failed to start recording",s)}},[c,r]),E=e.useCallback(()=>{u.current&&u.current.state!=="inactive"&&u.current.stop()},[]),v=e.useCallback(()=>{u.current&&u.current.state==="recording"&&(u.current.pause(),d(!0))},[]),b=e.useCallback(()=>{u.current&&u.current.state==="paused"&&(u.current.resume(),d(!1))},[]),f=e.useCallback(async()=>{if(!c)return null;const o=c.getVideoTracks()[0];if(!o)return null;const s=new window.ImageCapture(o);try{return await s.takePhoto()}catch{return null}},[c]),l=e.useCallback(()=>new Blob(w,{type:"video/webm"}),[w]);return{isRecording:r,isPaused:k,recordedBlob:w.length>0?l():null,startRecording:h,stopRecording:E,pauseRecording:v,resumeRecording:b,takeSnapshot:f,clearRecordings:()=>t([])}},Y=(c={})=>{const r=c.dbName||"CameraStore",i=c.storeName||"media",k=c.defaultRetentionMs||7*24*60*60*1e3,[d,w]=e.useState(!1),t=e.useCallback(()=>new Promise((f,l)=>{const o=indexedDB.open(r,2);o.onerror=()=>l(o.error),o.onsuccess=()=>f(o.result),o.onupgradeneeded=s=>{const a=s.target.result;if(!a.objectStoreNames.contains(i))a.createObjectStore(i,{keyPath:"id"}).createIndex("expiresAt","expiresAt",{unique:!1});else{const m=s.target.transaction.objectStore(i);m.indexNames.contains("expiresAt")||m.createIndex("expiresAt","expiresAt",{unique:!1})}}}),[r,i]),u=e.useCallback(async()=>{try{const o=(await t()).transaction(i,"readwrite").objectStore(i),s=o.index("expiresAt"),a=Date.now(),p=IDBKeyRange.upperBound(a),m=s.openCursor(p);m.onsuccess=R=>{const C=R.target.result;C&&(console.log(`[useStorage] Auto-deleting expired item: ${C.primaryKey}`),o.delete(C.primaryKey),C.continue())}}catch(f){console.warn("Failed to prune expired items",f)}},[t,i]);e.useEffect(()=>{u()},[u]);const y=e.useCallback(async(f,l,o)=>{try{const s=await t();return new Promise((a,p)=>{const R=s.transaction(i,"readwrite").objectStore(i),C=(o==null?void 0:o.retentionMs)||k,N=Date.now()+C,O={id:l,blob:f,date:new Date().toISOString(),type:f.type,expiresAt:N},L=R.put(O);L.onsuccess=()=>a(),L.onerror=()=>p(L.error)})}catch(s){throw console.error("Save failed",s),s}},[t,i,k]),h=e.useCallback(async f=>{try{const l=await t();return new Promise((o,s)=>{const m=l.transaction(i,"readonly").objectStore(i).get(f);m.onsuccess=()=>o(m.result?m.result.blob:null),m.onerror=()=>s(m.error)})}catch{return null}},[t,i]),E=e.useCallback(async f=>{const l=await t();return new Promise((o,s)=>{const m=l.transaction(i,"readwrite").objectStore(i).delete(f);m.onsuccess=()=>o(),m.onerror=()=>s(m.error)})},[t,i]),v=e.useCallback(async f=>{const l=await h(f);if(!l)throw new Error("File not found");const o=URL.createObjectURL(l),s=document.createElement("a");s.href=o,s.download=f,document.body.appendChild(s),s.click(),document.body.removeChild(s),URL.revokeObjectURL(o)},[h]),b=e.useCallback(async(f,l)=>(w(!0),new Promise((o,s)=>{const a=new XMLHttpRequest;a.open(l.method||"PUT",l.url),l.withCredentials&&(a.withCredentials=!0),l.timeout&&(a.timeout=l.timeout),l.headers&&Object.entries(l.headers).forEach(([p,m])=>a.setRequestHeader(p,m)),a.upload.onprogress=p=>{if(p.lengthComputable&&l.onProgress){const m=Math.round(p.loaded/p.total*100);l.onProgress(m)}},a.onload=()=>{w(!1),a.status>=200&&a.status<300?o():s(new Error(`Upload failed: ${a.statusText}`))},a.onerror=()=>{w(!1),s(new Error("Network Error"))},a.ontimeout=()=>{w(!1),s(new Error("Upload Timed Out"))},a.send(f)})),[]);return{saveToLocal:y,getFromLocal:h,deleteFromLocal:E,downloadFromLocal:v,uploadToRemote:b,isUploading:d}},ne=(c={})=>{const{autoStart:r=!1,defaultConstraints:i={video:!0,audio:!0},mock:k=!1,autoRetry:d=!1,debug:w=!1}=c,[t,u]=e.useState(null),[y,h]=e.useState(null),[E,v]=e.useState(!1),[b,f]=e.useState(null),[l,o]=e.useState(null),s=e.useRef(0),a=e.useCallback((...n)=>{w&&console.log("[useSimpleCamera]",...n)},[w]),{videoDevices:p,audioDevices:m}=G(),R=$(t),C=Q(t),N=K(t),O=J(),L=Y(),[V,g]=e.useState(void 0),[P,A]=e.useState(void 0),I=X(t,{onMotion:V}),z=Z(t,{onDetect:n=>P==null?void 0:P(n)}),F=e.useCallback(n=>{let S="UNKNOWN_ERROR";n.name==="NotAllowedError"||n.name==="PermissionDeniedError"?S="PERMISSION_DENIED":n.name==="NotFoundError"||n.name==="DevicesNotFoundError"?S="NO_DEVICE_FOUND":n.name==="OverconstrainedError"&&(S="CONSTRAINT_ERROR");const x={type:S,message:n.message||"An unexpected error occurred",originalError:n};return a("Error encountered:",x),h(x),v(!1),x},[a]),oe=e.useCallback(async()=>{try{(await navigator.mediaDevices.getUserMedia({video:!0,audio:!0})).getTracks().forEach(S=>S.stop()),v(!0)}catch(n){F(n)}},[F]),re=n=>{switch(n){case"SD":return{width:640,height:480};case"HD":return{width:1280,height:720};case"FHD":return{width:1920,height:1080};case"4K":return{width:3840,height:2160};case"Instagram":return{aspectRatio:1/1};default:return{}}},se=()=>{const n=document.createElement("canvas");n.width=640,n.height=480;const S=n.getContext("2d");S&&(S.fillStyle="black",S.fillRect(0,0,640,480),S.fillStyle="white",S.font="48px sans-serif",S.fillText("MOCK CAMERA",150,240));const x=n.captureStream(30),B=new AudioContext().createMediaStreamDestination();return x.addTrack(B.stream.getAudioTracks()[0]),x},q=e.useCallback(async(n=i)=>{if(h(null),a("Starting camera...",n),k){a("Mock mode enabled. Generating synthetic stream."),u(se()),v(!0);return}try{let S;if("preset"in n){const{preset:T,deviceId:B}=n;f(T),S={video:{...re(T),deviceId:B?{exact:B}:void 0},audio:!0}}else S=n;const x=await navigator.mediaDevices.getUserMedia(S);if(u(x),v(!0),s.current=0,"wakeLock"in navigator)try{const T=await navigator.wakeLock.request("screen");o(T)}catch(T){console.warn("Wake Lock failed",T)}}catch(S){const x=F(S);if(d&&s.current<3&&x.type!=="PERMISSION_DENIED"){const T=(s.current+1)*1e3;a(`Auto-retrying in ${T}ms... (Attempt ${s.current+1}/3)`),s.current+=1,setTimeout(()=>q(n),T)}}},[i,k,d,F,a]),ae=e.useCallback(async()=>{try{const n=await navigator.mediaDevices.getDisplayMedia({video:!0,audio:!0});u(n),v(!0)}catch(n){F(n)}},[F]),H=e.useCallback(()=>{t&&(t.getTracks().forEach(n=>n.stop()),u(null),l&&(l.release().catch(console.error),o(null)))},[t,l]),ce=e.useCallback(n=>{t==null||t.getVideoTracks().forEach(S=>S.enabled=n)},[t]),ie=e.useCallback(n=>{t==null||t.getAudioTracks().forEach(S=>S.enabled=n)},[t]),ue=e.useCallback(async()=>{if(!t)return;const x=t.getVideoTracks()[0].getSettings().facingMode;H(),await q({video:{facingMode:{exact:x==="user"?"environment":"user"}},audio:!0})},[t,H,q]),le=e.useCallback(async n=>{try{document.pictureInPictureElement?await document.exitPictureInPicture():n&&await n.requestPictureInPicture()}catch(S){console.error("PiP failed",S)}},[]),de=e.useCallback(async n=>{if(!t)throw new Error("No stream");const S=t.getVideoTracks()[0],x="ImageCapture"in window?new window.ImageCapture(S):null,T=(n==null?void 0:n.mirror)||!1,B=(n==null?void 0:n.format)||"image/png",fe=(n==null?void 0:n.quality)||.92,j=(n==null?void 0:n.filter)||"none";let W;const ee=D=>{const _=document.createElement("canvas"),te="videoWidth"in D?D.videoWidth:D.width,me="videoHeight"in D?D.videoHeight:D.height;_.width=te,_.height=me;const U=_.getContext("2d");if(!U)throw new Error("No Canvas Context");return T&&(U.translate(te,0),U.scale(-1,1)),j!=="none"&&(U.filter=j==="grayscale"?"grayscale(100%)":j==="sepia"?"sepia(100%)":j==="contrast"?"contrast(150%)":j==="blur"?"blur(5px)":"none"),U.drawImage(D,0,0),new Promise(ge=>_.toBlob(we=>ge(we),B,fe))};if(x){const D=await x.grabFrame();W=await ee(D),D.close()}else{const D=document.createElement("video");D.srcObject=t,D.muted=!0,await D.play(),W=await ee(D),D.pause(),D.srcObject=null}return W?URL.createObjectURL(W):""},[t]);return e.useEffect(()=>()=>{t&&t.getTracks().forEach(n=>n.stop())},[]),e.useEffect(()=>{r&&!t&&q()},[r,q]),{stream:t,error:y,permissionGranted:E,videoDevices:p,audioDevices:m,activePreset:b,startCamera:q,stopCamera:H,acquirePermissions:oe,startScreenShare:ae,toggleVideo:ce,toggleAudio:ie,toggleFacingMode:ue,togglePiP:le,captureImage:de,controls:R,recorder:C,audioLevel:N,motionDetection:I,barcodeScanner:z,orientation:O,storage:L,setMotionCallback:g,setBarcodeCallback:A,isCameraActive:!!t}};M.useAudioLevel=K,M.useBarcodeScanner=Z,M.useCameraControls=$,M.useMediaDevices=G,M.useMotionDetection=X,M.useOrientation=J,M.useRecorder=Q,M.useSimpleCamera=ne,M.useStorage=Y,Object.defineProperty(M,Symbol.toStringTag,{value:"Module"})});