refactor: extract logic from QrScanner and UrlCleaner to composables
This commit is contained in:
110
src/composables/useCamera.js
Normal file
110
src/composables/useCamera.js
Normal file
@@ -0,0 +1,110 @@
|
||||
import { ref, watch, onUnmounted } from 'vue'
|
||||
|
||||
export function useCamera(videoRef) {
|
||||
const stream = ref(null)
|
||||
const facingMode = ref('environment')
|
||||
const hasMultipleCameras = ref(false)
|
||||
const isMirrored = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const checkCameras = async () => {
|
||||
try {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
|
||||
return
|
||||
}
|
||||
const devices = await navigator.mediaDevices.enumerateDevices()
|
||||
const cameras = devices.filter(d => d.kind === 'videoinput')
|
||||
hasMultipleCameras.value = cameras.length > 1
|
||||
} catch (e) {
|
||||
console.error('Error checking cameras:', e)
|
||||
}
|
||||
}
|
||||
|
||||
const stopCamera = () => {
|
||||
if (stream.value) {
|
||||
stream.value.getTracks().forEach(t => t.stop())
|
||||
stream.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const startCamera = async () => {
|
||||
stopCamera()
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const constraints = {
|
||||
video: {
|
||||
facingMode: facingMode.value,
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 }
|
||||
}
|
||||
}
|
||||
|
||||
const mediaStream = await navigator.mediaDevices.getUserMedia(constraints)
|
||||
stream.value = mediaStream
|
||||
|
||||
// Detect actual facing mode to mirror front camera correctly
|
||||
const videoTrack = mediaStream.getVideoTracks()[0]
|
||||
if (videoTrack) {
|
||||
const settings = videoTrack.getSettings()
|
||||
if (settings.facingMode) {
|
||||
isMirrored.value = settings.facingMode === 'user'
|
||||
} else {
|
||||
// Fallback: check label for desktop cameras or assume requested mode
|
||||
const label = videoTrack.label ? videoTrack.label.toLowerCase() : ''
|
||||
if (label.includes('front') || label.includes('facetime') || label.includes('macbook')) {
|
||||
isMirrored.value = true
|
||||
} else {
|
||||
isMirrored.value = facingMode.value === 'user'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (videoRef.value) {
|
||||
videoRef.value.srcObject = mediaStream
|
||||
return new Promise((resolve) => {
|
||||
videoRef.value.onloadedmetadata = () => {
|
||||
videoRef.value.play().catch(e => console.error('Play error', e))
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name === 'NotAllowedError') {
|
||||
error.value = 'Camera permission denied'
|
||||
} else if (err.name === 'NotFoundError') {
|
||||
error.value = 'No camera found'
|
||||
} else {
|
||||
error.value = `Camera error: ${err.name}`
|
||||
}
|
||||
throw err // Let caller know it failed
|
||||
}
|
||||
}
|
||||
|
||||
const switchCamera = () => {
|
||||
facingMode.value = facingMode.value === 'environment' ? 'user' : 'environment'
|
||||
}
|
||||
|
||||
watch(facingMode, () => {
|
||||
if (stream.value) {
|
||||
// Re-start if already running
|
||||
startCamera().catch(() => { })
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopCamera()
|
||||
})
|
||||
|
||||
return {
|
||||
stream,
|
||||
facingMode,
|
||||
hasMultipleCameras,
|
||||
isMirrored,
|
||||
error,
|
||||
checkCameras,
|
||||
startCamera,
|
||||
stopCamera,
|
||||
switchCamera
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user