lint / go (push) Canceled after 0s
lint / go_mod (push) Canceled after 0s
lint / conf (push) Canceled after 0s
lint / docslinks (push) Canceled after 0s
lint / docsorder (push) Canceled after 0s
lint / apidocs (push) Canceled after 0s
lint / other (push) Canceled after 0s
test / test_64 (push) Canceled after 0s
test / test_32 (push) Canceled after 0s
test / test_e2e (push) Canceled after 0s
257 lines
11 KiB
HTML
257 lines
11 KiB
HTML
<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="color-scheme" content="light">
|
|
<title>MediaMTX Stream Console</title>
|
|
<link rel="stylesheet" href="./app.css">
|
|
<script src="./list-tools.js" defer></script>
|
|
<script src="./app.js" defer></script>
|
|
</head>
|
|
<body>
|
|
<a id="skip-link" class="skip-link" href="#login-main">跳到主要内容</a>
|
|
|
|
<div id="login-view" class="login-shell">
|
|
<main id="login-main" class="login-card" tabindex="-1">
|
|
<div class="login-heading">
|
|
<p class="eyebrow">MediaMTX</p>
|
|
<h1>登录 Stream Console</h1>
|
|
<p>使用 <code>mediamtx.yml</code> 中具有 <code>action: api</code> 权限的账号。</p>
|
|
</div>
|
|
|
|
<div class="security-note" role="note">
|
|
<strong>当前连接使用 HTTP</strong>
|
|
<span>登录信息不会被加密,请仅在可信局域网中使用。</span>
|
|
</div>
|
|
|
|
<form id="login-form" class="login-form" novalidate>
|
|
<div class="field">
|
|
<label for="login-username">用户名</label>
|
|
<input id="login-username" name="username" type="text" required autocomplete="username" autocapitalize="none" spellcheck="false">
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label for="login-password">密码</label>
|
|
<div class="password-field">
|
|
<input id="login-password" name="password" type="password" required autocomplete="current-password" aria-describedby="login-error">
|
|
<button id="login-password-toggle" class="button button-secondary" type="button" aria-pressed="false">显示</button>
|
|
</div>
|
|
</div>
|
|
|
|
<p id="login-error" class="login-error" role="alert" aria-live="assertive"></p>
|
|
<button id="login-submit" class="button button-primary login-submit" type="submit">登录</button>
|
|
</form>
|
|
</main>
|
|
</div>
|
|
|
|
<div id="app-view" hidden>
|
|
|
|
<header class="topbar">
|
|
<h1>MediaMTX 设备列表</h1>
|
|
<div class="topbar-actions" aria-label="服务操作">
|
|
<span id="api-status" class="status-pill status-loading" role="status">正在连接 API</span>
|
|
<button id="refresh-button" class="button button-secondary" type="button">刷新</button>
|
|
<button id="add-button" class="button button-primary" type="button">添加 RTSP 源</button>
|
|
<button id="logout-button" class="button button-secondary" type="button">退出登录</button>
|
|
</div>
|
|
</header>
|
|
|
|
<main id="main-content" class="page" tabindex="-1">
|
|
<section class="stats" aria-label="路径概览">
|
|
<article class="stat-card">
|
|
<span>路径总数</span>
|
|
<strong id="stat-total">—</strong>
|
|
</article>
|
|
<article class="stat-card">
|
|
<span>在线</span>
|
|
<strong id="stat-online">—</strong>
|
|
</article>
|
|
<article class="stat-card">
|
|
<span>离线</span>
|
|
<strong id="stat-offline">—</strong>
|
|
</article>
|
|
<article class="stat-card">
|
|
<span>待请求</span>
|
|
<strong id="stat-idle">—</strong>
|
|
</article>
|
|
</section>
|
|
|
|
<section class="paths-panel" aria-label="RTSP 设备列表">
|
|
<div class="panel-heading">
|
|
<div class="filters">
|
|
<label class="search-field" for="search-input">
|
|
<span>搜索</span>
|
|
<input id="search-input" type="search" placeholder="路径名称或地址" autocomplete="off">
|
|
</label>
|
|
<label for="status-filter">
|
|
<span>状态</span>
|
|
<select id="status-filter">
|
|
<option value="all">全部</option>
|
|
<option value="online">在线</option>
|
|
<option value="offline">离线</option>
|
|
<option value="idle">待请求</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-alert" class="page-alert" role="alert" hidden>
|
|
<div>
|
|
<strong id="page-alert-title"></strong>
|
|
<p id="page-alert-message"></p>
|
|
</div>
|
|
<button id="alert-retry-button" class="button button-secondary" type="button">重试</button>
|
|
</div>
|
|
|
|
<div id="loading-state" class="loading-state" aria-live="polite">
|
|
<span class="spinner" aria-hidden="true"></span>
|
|
<span id="loading-text">正在加载设备数据…</span>
|
|
</div>
|
|
|
|
<div id="empty-state" class="empty-state" hidden>
|
|
<h3 id="empty-title">还没有 RTSP 路径</h3>
|
|
<p id="empty-message">添加第一个摄像头或 RTSP 服务地址,之后可在列表中预览与管理。</p>
|
|
<button id="empty-add-button" class="button button-primary" type="button">添加 RTSP 源</button>
|
|
</div>
|
|
|
|
<div id="paths-content" hidden>
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">路径</th>
|
|
<th scope="col">状态</th>
|
|
<th scope="col">源地址</th>
|
|
<th scope="col">传输</th>
|
|
<th scope="col">读取者</th>
|
|
<th scope="col"><span class="visually-hidden">操作</span></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="paths-table-body"></tbody>
|
|
</table>
|
|
</div>
|
|
<div id="paths-card-list" class="card-list"></div>
|
|
<nav id="path-pagination" class="pagination" aria-label="设备列表分页">
|
|
<p id="pagination-summary" class="pagination-summary" aria-live="polite"></p>
|
|
<div class="pagination-controls">
|
|
<label for="page-size">
|
|
<span>每页</span>
|
|
<select id="page-size">
|
|
<option value="25">25</option>
|
|
<option value="50" selected>50</option>
|
|
<option value="100">100</option>
|
|
</select>
|
|
</label>
|
|
<button id="previous-page" class="button button-secondary" type="button">上一页</button>
|
|
<span id="pagination-page" class="pagination-page"></span>
|
|
<button id="next-page" class="button button-secondary" type="button">下一页</button>
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<dialog id="path-dialog" class="modal">
|
|
<form id="path-form" method="dialog" novalidate>
|
|
<div class="modal-header">
|
|
<div>
|
|
<p class="eyebrow">路径配置</p>
|
|
<h2 id="path-dialog-title">添加 RTSP 源</h2>
|
|
</div>
|
|
<button class="icon-button" type="button" data-close-dialog="path-dialog" aria-label="关闭路径配置">关闭</button>
|
|
</div>
|
|
|
|
<div id="form-alert" class="form-alert" role="alert" hidden></div>
|
|
|
|
<div class="field">
|
|
<label for="path-name">路径名称 <span aria-hidden="true">*</span></label>
|
|
<input id="path-name" name="pathName" type="text" required autocomplete="off" aria-describedby="path-name-help path-name-error">
|
|
<p id="path-name-help" class="helper">允许使用多级名称,例如 <code>building/east</code>。</p>
|
|
<p id="path-name-error" class="field-error" role="alert"></p>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label for="source-url">RTSP 源地址 <span id="source-required" aria-hidden="true">*</span></label>
|
|
<div class="password-field">
|
|
<input id="source-url" name="source" type="password" inputmode="url" autocomplete="new-password" spellcheck="false" aria-describedby="source-help source-error">
|
|
<button id="source-toggle" class="button button-secondary" type="button" aria-pressed="false">显示</button>
|
|
</div>
|
|
<p id="source-help" class="helper">例如 rtsp://user:password@camera.local/live。凭据不会持久化到浏览器。</p>
|
|
<p id="source-error" class="field-error" role="alert"></p>
|
|
</div>
|
|
|
|
<div class="field-grid">
|
|
<div class="field">
|
|
<label for="transport">RTSP 传输协议</label>
|
|
<select id="transport" name="transport">
|
|
<option value="automatic">自动</option>
|
|
<option value="tcp">TCP</option>
|
|
<option value="udp">UDP</option>
|
|
</select>
|
|
</div>
|
|
<label class="switch-field" for="source-on-demand">
|
|
<input id="source-on-demand" name="sourceOnDemand" type="checkbox">
|
|
<span>
|
|
<strong>按需拉流</strong>
|
|
<small>仅在有读取者时连接上游。</small>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="modal-actions">
|
|
<button class="button button-secondary" type="button" data-close-dialog="path-dialog">取消</button>
|
|
<button id="path-submit" class="button button-primary" type="submit">添加路径</button>
|
|
</div>
|
|
</form>
|
|
</dialog>
|
|
|
|
<dialog id="delete-dialog" class="modal modal-small">
|
|
<form id="delete-form" method="dialog">
|
|
<div class="modal-header">
|
|
<div>
|
|
<p class="eyebrow danger-text">危险操作</p>
|
|
<h2>删除路径</h2>
|
|
</div>
|
|
<button class="icon-button" type="button" data-close-dialog="delete-dialog" aria-label="关闭删除确认">关闭</button>
|
|
</div>
|
|
<p>确定删除 <strong id="delete-path-name"></strong>?该路径的当前读取连接会被中断。</p>
|
|
<div id="delete-alert" class="form-alert" role="alert" hidden></div>
|
|
<div class="modal-actions">
|
|
<button class="button button-secondary" type="button" data-close-dialog="delete-dialog">取消</button>
|
|
<button id="delete-submit" class="button button-danger" type="submit">删除路径</button>
|
|
</div>
|
|
</form>
|
|
</dialog>
|
|
|
|
<dialog id="preview-dialog" class="modal modal-wide">
|
|
<div class="modal-header">
|
|
<div>
|
|
<p class="eyebrow">媒体预览</p>
|
|
<h2 id="preview-title">预览</h2>
|
|
</div>
|
|
<button class="icon-button" type="button" data-close-dialog="preview-dialog" aria-label="关闭视频预览">关闭</button>
|
|
</div>
|
|
|
|
<div class="preview-tabs" role="tablist" aria-label="预览协议">
|
|
<button id="webrtc-tab" class="tab" type="button" role="tab" aria-selected="true" aria-controls="preview-frame">WebRTC</button>
|
|
<button id="hls-tab" class="tab" type="button" role="tab" aria-selected="false" aria-controls="preview-frame">HLS</button>
|
|
</div>
|
|
|
|
<div class="preview-layout">
|
|
<div class="preview-frame-wrap">
|
|
<iframe id="preview-frame" title="MediaMTX 视频预览" loading="lazy" allow="autoplay; fullscreen; picture-in-picture" referrerpolicy="no-referrer"></iframe>
|
|
<p id="preview-help" class="helper">WebRTC 低延迟优先;连接失败时可切换到 HLS。</p>
|
|
</div>
|
|
<aside class="output-panel" aria-labelledby="output-heading">
|
|
<h3 id="output-heading">输出地址</h3>
|
|
<div id="output-addresses"></div>
|
|
</aside>
|
|
</div>
|
|
</dialog>
|
|
|
|
<div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
|
|
</div>
|
|
</body>
|
|
</html>
|