1+ const startBtn = document . getElementById ( 'startBtn' ) ;
2+ const stopBtn = document . getElementById ( 'stopBtn' ) ;
3+ const video = document . getElementById ( 'video' ) ;
4+ const deviceSelect = document . getElementById ( 'deviceSelect' ) ;
5+ const mirrorToggle = document . getElementById ( 'mirrorToggle' ) ;
6+ const captureBtn = document . getElementById ( 'captureBtn' ) ;
7+ const canvas = document . getElementById ( 'canvas' ) ;
8+ const downloadLink = document . getElementById ( 'downloadLink' ) ;
9+ const status = document . getElementById ( 'status' ) ;
10+
11+ let stream = null ;
12+
13+ function setStatus ( s ) { status . textContent = s ; }
14+
15+ async function enumerateCameras ( ) {
16+ try {
17+ const devices = await navigator . mediaDevices . enumerateDevices ( ) ;
18+ const cams = devices . filter ( d => d . kind === 'videoinput' ) ;
19+ deviceSelect . innerHTML = '' ;
20+ cams . forEach ( ( c , i ) => {
21+ const opt = document . createElement ( 'option' ) ;
22+ opt . value = c . deviceId ;
23+ opt . text = c . label || `Camera ${ i + 1 } ` ;
24+ deviceSelect . appendChild ( opt ) ;
25+ } ) ;
26+ if ( cams . length === 0 ) deviceSelect . innerHTML = '<option disabled>No cameras found</option>' ;
27+ } catch ( err ) {
28+ console . error ( 'Cannot list devices' , err ) ;
29+ deviceSelect . innerHTML = '<option disabled>Unable to enumerate devices</option>' ;
30+ }
31+ }
32+
33+ async function startCamera ( deviceId ) {
34+ stopCamera ( ) ;
35+ setStatus ( 'Requesting camera...' ) ;
36+ const constraints = {
37+ audio : false ,
38+ video : {
39+ width : { ideal : 1280 } ,
40+ height : { ideal : 720 } ,
41+ }
42+ } ;
43+ if ( deviceId ) constraints . video . deviceId = { exact : deviceId } ;
44+ try {
45+ stream = await navigator . mediaDevices . getUserMedia ( constraints ) ;
46+ video . srcObject = stream ;
47+ startBtn . disabled = true ;
48+ stopBtn . disabled = false ;
49+ captureBtn . disabled = false ;
50+ setStatus ( 'Camera started' ) ;
51+ await enumerateCameras ( ) ; // refresh labels (some browsers only expose labels after permission)
52+ applyMirror ( ) ;
53+ } catch ( err ) {
54+ console . error ( 'getUserMedia error' , err ) ;
55+ setStatus ( 'Camera error: ' + ( err . message || err . name ) ) ;
56+ }
57+ }
58+
59+ function stopCamera ( ) {
60+ if ( stream ) {
61+ stream . getTracks ( ) . forEach ( t => t . stop ( ) ) ;
62+ stream = null ;
63+ video . srcObject = null ;
64+ startBtn . disabled = false ;
65+ stopBtn . disabled = true ;
66+ captureBtn . disabled = true ;
67+ setStatus ( 'Camera stopped' ) ;
68+ }
69+ }
70+
71+ function applyMirror ( ) {
72+ const mirrored = mirrorToggle . checked ;
73+ // For the live preview, the easiest and smoothest approach is CSS transform.
74+ // This flips the DOM element visually but does not change the underlying camera frames.
75+ video . style . transform = mirrored ? 'scaleX(-1)' : 'none' ;
76+ }
77+
78+ function captureSnapshot ( ) {
79+ if ( ! video || video . readyState < 2 ) return ;
80+ const w = canvas . width = video . videoWidth || 320 ;
81+ const h = canvas . height = video . videoHeight || 240 ;
82+ const ctx = canvas . getContext ( '2d' ) ;
83+
84+ ctx . save ( ) ;
85+ if ( mirrorToggle . checked ) {
86+ // To make the saved image match the mirrored preview, draw the video flipped on the canvas.
87+ ctx . translate ( w , 0 ) ;
88+ ctx . scale ( - 1 , 1 ) ;
89+ }
90+ // draw the video frame to canvas
91+ ctx . drawImage ( video , 0 , 0 , w , h ) ;
92+ ctx . restore ( ) ;
93+
94+ // Create a download link for the snapshot
95+ canvas . toBlob ( blob => {
96+ if ( ! blob ) return ;
97+ const url = URL . createObjectURL ( blob ) ;
98+ downloadLink . href = url ;
99+ downloadLink . style . display = 'inline-block' ;
100+ downloadLink . textContent = 'Download snapshot' ;
101+ } , 'image/png' ) ;
102+
103+ setStatus ( 'Snapshot captured' ) ;
104+ }
105+
106+ // Wire up UI
107+ startBtn . addEventListener ( 'click' , async ( ) => {
108+ const selected = deviceSelect . value || null ;
109+ await startCamera ( selected ) ;
110+ } ) ;
111+ stopBtn . addEventListener ( 'click' , ( ) => stopCamera ( ) ) ;
112+ mirrorToggle . addEventListener ( 'change' , applyMirror ) ;
113+ captureBtn . addEventListener ( 'click' , captureSnapshot ) ;
114+
115+ // If the user changes camera from the dropdown, restart with that device
116+ deviceSelect . addEventListener ( 'change' , async ( ) => {
117+ if ( deviceSelect . value ) await startCamera ( deviceSelect . value ) ;
118+ } ) ;
119+
120+ // On load: try to enumerate devices and set a helpful default
121+ ( async function init ( ) {
122+ if ( ! navigator . mediaDevices || ! navigator . mediaDevices . getUserMedia ) {
123+ setStatus ( 'getUserMedia not supported in this browser' ) ;
124+ startBtn . disabled = true ;
125+ return ;
126+ }
127+ await enumerateCameras ( ) ;
128+ // Try to pre-select a camera if available
129+ if ( deviceSelect . options . length > 0 && deviceSelect . options [ 0 ] . value ) {
130+ deviceSelect . selectedIndex = 0 ;
131+ }
132+ setStatus ( 'Ready — click "Start Camera"' ) ;
133+ } ) ( ) ;
134+
135+ // Optional: stop camera when the page is hidden to be polite with permissions
136+ document . addEventListener ( 'visibilitychange' , ( ) => {
137+ if ( document . hidden ) stopCamera ( ) ;
138+ } ) ;
0 commit comments