Files
carrot 5d3546b8da CD210 model, WebCarrotMan, fix SpeedTF (#248)
* scc_contro: a_val=> no jerk limitation.

* fix..

* fix..

* fix..

* test. for isg

* carrot web server

* fix.. change branches..

* fix..

* CD210 model..

* fix..

* fix..

* ff

* fix..

* test full screen

* fix..

* fix..

* disable webrtc

* fix..

* fix.. enableWebRTC,

* fix..

* revert dm model.

* register my ip

* fix..

* test

* fix..

* fix..

* fix.. speed TF, more TFs. 0, 10, 20, 30%

* fix..

* fix..

* fix..

* fix..

* fix..

* fix

* fix..

* fix..

* ff

* ff

* fix.. quick link

* fix..

* fix..

* fix quick link... text..

* fix SCC error (canfd)

* fix.. speed TF < 0

* hud

* fix..

* fix.. tf..

* fleet..

* release..

* web cmd

* fix run cmd..

* fix cmd

* cmd allow

* fix..

* fix.. release..
2026-02-13 11:16:13 +09:00

218 lines
6.5 KiB
HTML

<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>webrtcd browser test</title>
<style>
body {
font-family: sans-serif;
margin: 16px;
}
video {
width: 48%;
background: #000;
margin: 4px;
}
.row {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
pre {
background: #111;
color: #0f0;
padding: 8px;
height: 220px;
overflow: auto;
}
input, button {
padding: 6px;
margin: 4px 0;
}
</style>
</head>
<body>
<h2>webrtcd browser test</h2>
<label>Device IP:Port (webrtcd)</label><br />
<input id="baseUrl" style="width:420px" value="http://192.168.0.171:5001" />
<br />
<label>Cameras (comma separated)</label><br />
<input id="cameras" style="width:420px" value="road" />
<div style="opacity:0.8">예: road / wideRoad / driver 등 (서버가 기대하는 개수와 맞추세요)</div>
<label><input type="checkbox" id="sendMic" /> Send microphone to device (incoming_audio_track)</label><br />
<label><input type="checkbox" id="recvAudio" checked /> Receive audio from device (expected_audio_track)</label><br />
<label><input type="checkbox" id="useDataChannel" checked /> Create datachannel (messaging)</label><br />
<button id="btnStart">Start</button>
<button id="btnStop" disabled>Stop</button>
<h3>Remote streams</h3>
<div class="row" id="videos"></div>
<h3>Log</h3>
<pre id="log"></pre>
<script>
const logEl = document.getElementById("log");
function log(...args) {
const s = args.map(a => (typeof a === "string" ? a : JSON.stringify(a))).join(" ");
console.log(s);
logEl.textContent += s + "\n";
logEl.scrollTop = logEl.scrollHeight;
}
let pc = null;
let dc = null;
let localStream = null;
async function start() {
const baseUrl = document.getElementById("baseUrl").value.trim().replace(/\/+$/, "");
const cameras = document.getElementById("cameras").value.split(",").map(s => s.trim()).filter(Boolean);
const sendMic = document.getElementById("sendMic").checked;
const recvAudio = document.getElementById("recvAudio").checked;
const useDataChannel = document.getElementById("useDataChannel").checked;
if (!baseUrl) return log("baseUrl empty");
if (!cameras.length) return log("cameras empty");
pc = new RTCPeerConnection({
// 같은 LAN이면 STUN 없어도 되는 경우가 많습니다.
iceServers: []
});
pc.oniceconnectionstatechange = () => log("ICE:", pc.iceConnectionState);
pc.onconnectionstatechange = () => log("PC :", pc.connectionState);
// --- Remote track handler ---
const videosDiv = document.getElementById("videos");
videosDiv.innerHTML = "";
let videoCount = 0;
pc.ontrack = (ev) => {
log("ontrack kind=", ev.track.kind, "streams=", ev.streams.length);
const stream = ev.streams[0] || new MediaStream([ev.track]);
if (ev.track.kind === "video") {
videoCount++;
const v = document.createElement("video");
v.autoplay = true;
v.playsInline = true;
v.controls = true;
v.muted = true; // 브라우저 자동재생 정책 회피 (영상은 mute)
v.srcObject = stream;
v.title = "remote video " + videoCount;
videosDiv.appendChild(v);
} else if (ev.track.kind === "audio") {
// 오디오는 mute 하면 안 들리니 별도 오디오 엘리먼트
const a = document.createElement("audio");
a.autoplay = true;
a.controls = true;
a.srcObject = stream;
videosDiv.appendChild(a);
}
};
// --- DataChannel (optional) ---
if (useDataChannel) {
dc = pc.createDataChannel("messaging"); // label이 서버 기대와 다르면 안 붙을 수 있음
dc.onopen = () => log("DC open");
dc.onclose = () => log("DC close");
dc.onerror = (e) => log("DC error", e);
dc.onmessage = (e) => {
// 서버는 json bytes를 보냅니다.
try {
const txt = typeof e.data === "string" ? e.data : new TextDecoder().decode(e.data);
const obj = JSON.parse(txt);
log("DC msg:", obj.type);
} catch (err) {
log("DC raw msg:", e.data);
}
};
}
// --- Transceivers: video recvonly for each camera ---
for (let i = 0; i < cameras.length; i++) {
pc.addTransceiver("video", { direction: "recvonly" });
}
// --- Audio: receive from device (expected_audio_track) ---
if (recvAudio) {
pc.addTransceiver("audio", { direction: "recvonly" });
}
// --- Audio: send mic to device (incoming_audio_track) ---
if (sendMic) {
// getUserMedia는 보통 HTTPS 또는 localhost에서만 됩니다.
// (LAN IP로 열면 막힐 수 있어요)
localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
for (const track of localStream.getAudioTracks()) {
pc.addTrack(track, localStream);
}
}
// Create offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// Wait ICE gathering complete (간단히)
await new Promise((resolve) => {
if (pc.iceGatheringState === "complete") return resolve();
pc.onicegatheringstatechange = () => {
if (pc.iceGatheringState === "complete") resolve();
};
});
log("POST /stream ...");
const res = await fetch(baseUrl + "/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sdp: pc.localDescription.sdp,
cameras: cameras,
bridge_services_in: [], // 필요하면 예: ["controlsState"]
bridge_services_out: [] // 필요하면 예: ["carState","deviceState"]
})
});
if (!res.ok) {
const t = await res.text();
throw new Error("stream failed: " + res.status + " " + t);
}
const answer = await res.json();
log("answer type:", answer.type);
await pc.setRemoteDescription({ type: answer.type, sdp: answer.sdp });
document.getElementById("btnStart").disabled = true;
document.getElementById("btnStop").disabled = false;
log("started.");
}
async function stop() {
if (dc) { try { dc.close(); } catch(e) {} dc = null; }
if (pc) {
try { pc.getSenders().forEach(s => s.track && s.track.stop && s.track.stop()); } catch(e) {}
try { pc.close(); } catch(e) {}
pc = null;
}
if (localStream) {
localStream.getTracks().forEach(t => t.stop());
localStream = null;
}
document.getElementById("btnStart").disabled = false;
document.getElementById("btnStop").disabled = true;
log("stopped.");
}
document.getElementById("btnStart").onclick = () => start().catch(e => log("ERR:", e.message));
document.getElementById("btnStop").onclick = () => stop();
</script>
</body>
</html>