diff --git a/index.html b/index.html new file mode 100644 index 0000000..16be29c --- /dev/null +++ b/index.html @@ -0,0 +1,27 @@ + + +
+ +Not supported by your browser :( Try in Chromium desktop!
' + +async function capture() { + if (capture_started) + return + capture_started = true + let stream + try { + stream = await navigator.mediaDevices.getDisplayMedia({ + preferCurrentTab: true + }) + } catch (e) { + console.error(e) + if (e instanceof TypeError) + out_video.outerHTML = unsupported + else + capture_started = false + return + } + const [track] = stream.getVideoTracks() + track.addEventListener('ended', () => capture_started = false) + try { + // Enable chrome://flags/#element-capture - this will also enable fullscreen zoom of output + // See: https://developer.chrome.com/docs/web-platform/element-capture + // Note that pinch zoom pauses the stream: https://issuetracker.google.com/issues/337337168 + const restrictionTarget = await RestrictionTarget.fromElement(orig_video) + await track.restrictTo(restrictionTarget) + out_container.oncontextmenu = e => toggle_fullscreen(e) + out_container.title = 'Right-click to enter/exit fullscreen' + } catch (e) { + console.error(e) + try { + const cropTarget = await CropTarget.fromElement(orig_video) + await track.cropTo(cropTarget) + } catch (e){ + console.error(e) + out_video.outerHTML = unsupported + return + } + } + + const vision = await FilesetResolver.forVisionTasks('https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.12/wasm') + const poseLandmarker = await PoseLandmarker.createFromOptions( + vision, + { + baseOptions: { + modelAssetPath: 'https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_lite/float16/1/pose_landmarker_lite.task', + delegate: 'GPU' + }, + runningMode: 'VIDEO', + numPoses: 2 + }) + const canvasCtx = canvas.getContext('2d'); + const drawingUtils = new DrawingUtils(canvasCtx); + + const trackProcessor = new MediaStreamTrackProcessor({ track: track }) + const trackGenerator = new MediaStreamTrackGenerator({ kind: 'video' }) + let yuv, rgba + const transformer = new TransformStream({ + async transform(videoFrame, controller) { + const W = videoFrame.codedWidth + const H = videoFrame.codedHeight + canvasCtx.clearRect(0, 0, canvas.width, canvas.height) + + let rgba = new Uint8Array(W * H * 4) + if (effect.value.includes('landmarks')) { + canvasCtx.save() + effect_funcs['pose_landmarks'](videoFrame, poseLandmarker, drawingUtils) + canvasCtx.restore() + } + if (effect.value == 'pose_landmarks') + rgba = rgba.map((_, i) => ((i+1) % 4 == 0) * 255) + else { + const yuv = new Uint8Array(W * H * 1.5) + const copyResult = await videoFrame.copyTo(yuv) + const { stride, offset: Voffset } = copyResult[1] + const { offset: Uoffset } = copyResult[2] + rgba = effect_funcs[effect.value](W, H, stride, Voffset, Uoffset, yuv, rgba) + } + const init = { + codedHeight: H, + codedWidth: W, + format: 'RGBA', + timestamp: videoFrame.timestamp, + } + videoFrame.close() + controller.enqueue(new VideoFrame(rgba, init)) + } + }) + const transformed = trackProcessor.readable.pipeThrough(transformer).pipeTo(trackGenerator.writable) + out_video.srcObject = new MediaStream([trackGenerator]) +} + + +// FULLSCREEN + + +let wake_lock + + +function request_wake_lock() { + navigator.wakeLock?.request('screen').then(lock => wake_lock = lock).catch(e => console.error(e.message)) +} + + +function visibility_change_handler() { + if (wake_lock && document.visibilityState == 'visible') + request_wake_lock() +} + + +function toggle_fullscreen(event_or_elem, landscape=true, elem) { + if (event_or_elem.preventDefault) + event_or_elem.preventDefault() + elem ??= event_or_elem?.currentTarget || event_or_elem + const was_not_fullscreen_before = !document.fullscreenElement + if (was_not_fullscreen_before) { + if (!elem.dataset.has_fullscreen_handler) { + elem.dataset.has_fullscreen_handler = true + elem.addEventListener('fullscreenchange', () => { + if (elem.classList.toggle('fullscreen')) { + if (landscape) + screen.orientation.lock('landscape').catch(e => console.error(e.message)) // Works only in Chrome Android. See: https://bugzilla.mozilla.org/show_bug.cgi?id=1744125 + request_wake_lock() + document.addEventListener('visibilitychange', visibility_change_handler) + } else + wake_lock?.release().then(() => wake_lock = null) + }) + } + elem.requestFullscreen({navigationUI: 'hide'}).catch(e => console.error(e.message)) + } else + document.exitFullscreen() + return was_not_fullscreen_before +} \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..e0844ac --- /dev/null +++ b/style.css @@ -0,0 +1,45 @@ +body { + align-items: center; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 2rem; + height: 100vh; + height: 100dvh; + justify-content: center; + margin: 0; + padding-block: 2rem; +} + +iframe { + isolation: isolate; +} + +iframe, #out_container { + aspect-ratio: 16 / 9; + background-color: black; + border: none; + flex-shrink: 0; + height: 100%; + max-height: 40%; +} + +#inp_container { + display: flex; + gap: 1rem; +} + +#out_container { + color: white; + line-height: 3; + position: relative; + text-align: center; +} + +#out_container > * { + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; +} \ No newline at end of file