mirror of
https://github.com/comma-hacks/webrtc.git
synced 2025-10-01 22:42:09 +08:00
init
This commit is contained in:
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "cereal"]
|
||||
path = cereal
|
||||
url = https://github.com/commaai/cereal.git
|
26
bash_history.txt
Normal file
26
bash_history.txt
Normal file
@@ -0,0 +1,26 @@
|
||||
1 sudo apt update
|
||||
2 sudo apt upgrade
|
||||
12 sudo apt install vim git
|
||||
14 sudo apt install python3-distutils
|
||||
31 sudo apt update
|
||||
33 sudo apt install tmux apt git
|
||||
49 sudo apt install python-dev
|
||||
50 sudo apt install python3-dev
|
||||
91 sudo apt install iputils-tracepathj
|
||||
92 sudo apt install iputils-tracepath
|
||||
167 apt-get update && apt-get install -y --no-install-recommends autoconf build-essential ca-certificates capnproto clang cppcheck curl git libbz2-dev libcapnp-dev libffi-dev liblzma-dev libncurses5-dev libncursesw5-dev libreadline-dev libsqlite3-dev libssl-dev libtool libzmq3-dev llvm make ocl-icd-opencl-dev opencl-headers python-openssl tk-dev wget xz-utils zlib1g-dev
|
||||
168 sudo apt-get update && sudo apt-get install -y --no-install-recommends autoconf build-essential ca-certificates capnproto clang cppcheck curl git libbz2-dev libcapnp-dev libffi-dev liblzma-dev libncurses5-dev libncursesw5-dev libreadline-dev libsqlite3-dev libssl-dev libtool libzmq3-dev llvm make ocl-icd-opencl-dev opencl-headers python-openssl tk-dev wget xz-utils zlib1g-dev
|
||||
176 apt install libcapnp-dev
|
||||
177 sudo apt install libcapnp-dev
|
||||
178 apt install libcapnp-dev
|
||||
180 sudo apt install capnp
|
||||
181 sudo apt install capnproto
|
||||
183 sudo apt install clang
|
||||
185 sudo apt install libzmq3-dev
|
||||
187 sudo apt install opencl-headers
|
||||
190 sudo apt install ocl-icd-opencl-dev
|
||||
|
||||
# curl -sSL https://install.python-poetry.org | python3 -
|
||||
# poetry add ./cereal
|
||||
# poetry add pyyaml==5.1.2 Cython==0.29.14 scons==3.1.1 pycapnp==1.0.0 parameterized==0.7.4 numpy==1.21.1
|
||||
|
1
cereal
Submodule
1
cereal
Submodule
Submodule cereal added at 959ff79963
76
client.js
Normal file
76
client.js
Normal file
@@ -0,0 +1,76 @@
|
||||
var pc = null;
|
||||
|
||||
function negotiate() {
|
||||
pc.addTransceiver('video', {direction: 'recvonly'});
|
||||
pc.addTransceiver('audio', {direction: 'recvonly'});
|
||||
return pc.createOffer().then(function(offer) {
|
||||
return pc.setLocalDescription(offer);
|
||||
}).then(function() {
|
||||
// wait for ICE gathering to complete
|
||||
return new Promise(function(resolve) {
|
||||
if (pc.iceGatheringState === 'complete') {
|
||||
resolve();
|
||||
} else {
|
||||
function checkState() {
|
||||
if (pc.iceGatheringState === 'complete') {
|
||||
pc.removeEventListener('icegatheringstatechange', checkState);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
pc.addEventListener('icegatheringstatechange', checkState);
|
||||
}
|
||||
});
|
||||
}).then(function() {
|
||||
var offer = pc.localDescription;
|
||||
return fetch('/offer', {
|
||||
body: JSON.stringify({
|
||||
sdp: offer.sdp,
|
||||
type: offer.type,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: 'POST'
|
||||
});
|
||||
}).then(function(response) {
|
||||
return response.json();
|
||||
}).then(function(answer) {
|
||||
return pc.setRemoteDescription(answer);
|
||||
}).catch(function(e) {
|
||||
alert(e);
|
||||
});
|
||||
}
|
||||
|
||||
function start() {
|
||||
var config = {
|
||||
sdpSemantics: 'unified-plan'
|
||||
};
|
||||
|
||||
if (document.getElementById('use-stun').checked) {
|
||||
config.iceServers = [{urls: ['stun:stun.l.google.com:19302']}];
|
||||
}
|
||||
|
||||
pc = new RTCPeerConnection(config);
|
||||
|
||||
// connect audio / video
|
||||
pc.addEventListener('track', function(evt) {
|
||||
if (evt.track.kind == 'video') {
|
||||
document.getElementById('video').srcObject = evt.streams[0];
|
||||
} else {
|
||||
document.getElementById('audio').srcObject = evt.streams[0];
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('start').style.display = 'none';
|
||||
negotiate();
|
||||
document.getElementById('stop').style.display = 'inline-block';
|
||||
}
|
||||
|
||||
function stop() {
|
||||
document.getElementById('stop').style.display = 'none';
|
||||
|
||||
// close peer connection
|
||||
setTimeout(function() {
|
||||
pc.close();
|
||||
}, 500);
|
||||
}
|
84
compressed_vipc_track.py
Normal file
84
compressed_vipc_track.py
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
import cereal.messaging as messaging
|
||||
from aiortc import VideoStreamTrack
|
||||
import asyncio
|
||||
import numpy as np
|
||||
import av
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
W, H = 1928, 1208
|
||||
V4L2_BUF_FLAG_KEYFRAME = 8
|
||||
|
||||
class VisionIpcTrack(VideoStreamTrack):
|
||||
def __init__(self, sock_name, addr):
|
||||
super().__init__()
|
||||
self.codec = av.CodecContext.create("hevc", "r")
|
||||
os.environ["ZMQ"] = "1"
|
||||
messaging.context = messaging.Context()
|
||||
self.sock = messaging.sub_sock(sock_name, None, addr=addr, conflate=False)
|
||||
self.cnt = 0
|
||||
self.last_idx = -1
|
||||
self.seen_iframe = False
|
||||
self.time_q = []
|
||||
self.sock_name = sock_name
|
||||
|
||||
async def recv(self):
|
||||
pts, time_base = await self.next_timestamp()
|
||||
frame = None
|
||||
while frame is None:
|
||||
msgs = messaging.drain_sock(self.sock, wait_for_one=True)
|
||||
for evt in msgs:
|
||||
print(type(evt).__name__)
|
||||
evta = getattr(evt, evt.which())
|
||||
if evta.idx.encodeId != 0 and evta.idx.encodeId != (self.last_idx+1):
|
||||
print("DROP PACKET!")
|
||||
self.last_idx = evta.idx.encodeId
|
||||
if not self.seen_iframe and not (evta.idx.flags & V4L2_BUF_FLAG_KEYFRAME):
|
||||
print("waiting for iframe")
|
||||
continue
|
||||
self.time_q.append(time.monotonic())
|
||||
|
||||
# put in header (first)
|
||||
if not self.seen_iframe:
|
||||
self.codec.decode(av.packet.Packet(evta.header))
|
||||
self.seen_iframe = True
|
||||
|
||||
frames = self.codec.decode(av.packet.Packet(evta.data))
|
||||
if len(frames) == 0:
|
||||
print("DROP SURFACE")
|
||||
continue
|
||||
assert len(frames) == 1
|
||||
|
||||
frame = frames[0]
|
||||
frame.pts = pts
|
||||
frame.time_base = time_base
|
||||
return frame
|
||||
|
||||
if __name__ == "__main__":
|
||||
from time import time_ns
|
||||
import sys
|
||||
|
||||
async def test():
|
||||
frame_count=0
|
||||
start_time=time_ns()
|
||||
track = VisionIpcTrack("roadEncodeData", "192.168.99.200")
|
||||
while True:
|
||||
await track.recv()
|
||||
now = time_ns()
|
||||
playtime = now - start_time
|
||||
playtime_sec = playtime * 0.000000001
|
||||
if playtime_sec >= 1:
|
||||
print(f'fps: {frame_count}')
|
||||
frame_count = 0
|
||||
start_time = time_ns()
|
||||
else:
|
||||
frame_count+=1
|
||||
|
||||
# Run event loop
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
loop.run_until_complete(test())
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
40
index.html
Normal file
40
index.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>WebRTC webcam</title>
|
||||
<style>
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.option {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
#media {
|
||||
max-width: 1280px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="option">
|
||||
<input id="use-stun" type="checkbox" checked/>
|
||||
<label for="use-stun">Use STUN server</label>
|
||||
</div>
|
||||
<button id="start" onclick="start()">Start</button>
|
||||
<button id="stop" style="display: none" onclick="stop()">Stop</button>
|
||||
|
||||
<div id="media">
|
||||
<audio id="audio" autoplay="true"></audio>
|
||||
<video id="video" autoplay="true" playsinline="true"></video>
|
||||
</div>
|
||||
|
||||
<script src="client.js"></script>
|
||||
</body>
|
||||
</html>
|
1081
poetry.lock
generated
Normal file
1081
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
pyproject.toml
Normal file
23
pyproject.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[tool.poetry]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Your Name <you@example.com>"]
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9"
|
||||
scons = "3.1.1"
|
||||
pycapnp = "1.0.0"
|
||||
pyyaml = "5.1.2"
|
||||
cython = "0.29.14"
|
||||
parameterized = "0.7.4"
|
||||
numpy = "1.21.1"
|
||||
aiohttp = "^3.8.3"
|
||||
aiortc = "^1.3.2"
|
||||
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
121
server.py
Executable file
121
server.py
Executable file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import ssl
|
||||
from typing import OrderedDict
|
||||
from aiohttp import web
|
||||
from aiortc import RTCPeerConnection, RTCSessionDescription, RTCRtpCodecCapability
|
||||
from compressed_vipc_track import VisionIpcTrack
|
||||
|
||||
ROOT = os.path.dirname(__file__)
|
||||
|
||||
cams = ["roadEncodeData","wideRoadEncodeData","driverEncodeData"]
|
||||
|
||||
async def index(request):
|
||||
content = open(os.path.join(ROOT, "index.html"), "r").read()
|
||||
return web.Response(content_type="text/html", text=content)
|
||||
|
||||
async def javascript(request):
|
||||
content = open(os.path.join(ROOT, "client.js"), "r").read()
|
||||
return web.Response(content_type="application/javascript", text=content)
|
||||
|
||||
|
||||
async def offer(request):
|
||||
params = await request.json()
|
||||
offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"])
|
||||
|
||||
pc = RTCPeerConnection()
|
||||
pcs.add(pc)
|
||||
|
||||
@pc.on("connectionstatechange")
|
||||
async def on_connectionstatechange():
|
||||
print("Connection state is %s" % pc.connectionState)
|
||||
if pc.connectionState == "failed":
|
||||
await pc.close()
|
||||
pcs.discard(pc)
|
||||
|
||||
# TODO: stream the microphone
|
||||
audio = None
|
||||
|
||||
video = VisionIpcTrack(cams[int(args.cam)], args.addr)
|
||||
|
||||
video_sender = pc.addTrack(video)
|
||||
transceiver = next(t for t in pc.getTransceivers() if t.sender == video_sender)
|
||||
transceiver.setCodecPreferences(
|
||||
# [codec for codec in codecs if codec.mimeType == forced_codec]
|
||||
[RTCRtpCodecCapability(
|
||||
mimeType="video/H264",
|
||||
clockRate=90000,
|
||||
channels=None,
|
||||
parameters=OrderedDict([
|
||||
("packetization-mode", "1"),
|
||||
("level-asymmetry-allowed", "1"),
|
||||
("profile-level-id", "42001f"),
|
||||
])
|
||||
)]
|
||||
)
|
||||
|
||||
await pc.setRemoteDescription(offer)
|
||||
|
||||
answer = await pc.createAnswer()
|
||||
await pc.setLocalDescription(answer)
|
||||
|
||||
return web.Response(
|
||||
content_type="application/json",
|
||||
text=json.dumps(
|
||||
{"sdp": pc.localDescription.sdp, "type": pc.localDescription.type}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
pcs = set()
|
||||
|
||||
|
||||
async def on_shutdown(app):
|
||||
# close peer connections
|
||||
coros = [pc.close() for pc in pcs]
|
||||
await asyncio.gather(*coros)
|
||||
pcs.clear()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Decode video streams and broadcast via WebRTC")
|
||||
parser.add_argument("addr", help="Address of comma three")
|
||||
|
||||
# Not implemented (yet?). Geo already made the PoC for this, it should be possible.
|
||||
# parser.add_argument("--nvidia", action="store_true", help="Use nvidia instead of ffmpeg")
|
||||
|
||||
parser.add_argument("--cam", default="0", help="Camera to stream")
|
||||
|
||||
parser.add_argument("--cert-file", help="SSL certificate file (for HTTPS)")
|
||||
parser.add_argument("--key-file", help="SSL key file (for HTTPS)")
|
||||
parser.add_argument(
|
||||
"--host", default="0.0.0.0", help="Host for HTTP server (default: 0.0.0.0)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port", type=int, default=8080, help="Port for HTTP server (default: 8080)"
|
||||
)
|
||||
parser.add_argument("--verbose", "-v", action="count")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.verbose:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
else:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
if args.cert_file:
|
||||
ssl_context = ssl.SSLContext()
|
||||
ssl_context.load_cert_chain(args.cert_file, args.key_file)
|
||||
else:
|
||||
ssl_context = None
|
||||
|
||||
app = web.Application()
|
||||
app.on_shutdown.append(on_shutdown)
|
||||
app.router.add_get("/", index)
|
||||
app.router.add_get("/client.js", javascript)
|
||||
app.router.add_post("/offer", offer)
|
||||
web.run_app(app, host=args.host, port=args.port, ssl_context=ssl_context)
|
Reference in New Issue
Block a user